mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59:37 +02:00
Fix SEO and refactor HTML pages generation
* Split methods in multiple classes * Add JSONLD tags in embed too * Index embeds but use a canonical URL tag (targeting the watch page) * Remote objects don't include a canonical URL tag anymore. Instead we forbid indexation * Canonical URLs now use the official short URL (/w/, /w/p, /a, /c etc.)
This commit is contained in:
parent
e731f4b724
commit
f90db24233
23 changed files with 1876 additions and 1213 deletions
130
server/core/lib/html/shared/video-html.ts
Normal file
130
server/core/lib/html/shared/video-html.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { escapeHTML } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { toCompleteUUID } from '@server/helpers/custom-validators/misc.js'
|
||||
import express from 'express'
|
||||
import validator from 'validator'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { MEMOIZE_TTL, WEBSERVER } from '../../../initializers/constants.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { MVideo } from '../../../types/models/index.js'
|
||||
import { getActivityStreamDuration } from '../../activitypub/activity.js'
|
||||
import { isVideoInPrivateDirectory } from '../../video-privacy.js'
|
||||
import { Memoize } from '@server/helpers/memoize.js'
|
||||
import { MVideoThumbnailBlacklist } from 'server/dist/core/types/models/index.js'
|
||||
import { TagsHtml } from './tags-html.js'
|
||||
import { PageHtml } from './page-html.js'
|
||||
import { CommonEmbedHtml } from './common-embed-html.js'
|
||||
|
||||
export class VideoHtml {
|
||||
|
||||
static async getWatchVideoHTML (videoIdArg: string, req: express.Request, res: express.Response) {
|
||||
const videoId = toCompleteUUID(videoIdArg)
|
||||
|
||||
// Let Angular application handle errors
|
||||
if (!validator.default.isInt(videoId) && !validator.default.isUUID(videoId, 4)) {
|
||||
res.status(HttpStatusCode.NOT_FOUND_404)
|
||||
return PageHtml.getIndexHTML(req, res)
|
||||
}
|
||||
|
||||
const [ html, video ] = await Promise.all([
|
||||
PageHtml.getIndexHTML(req, res),
|
||||
VideoModel.loadWithBlacklist(videoId)
|
||||
])
|
||||
|
||||
// Let Angular application handle errors
|
||||
if (!video || isVideoInPrivateDirectory(video.privacy) || video.VideoBlacklist) {
|
||||
res.status(HttpStatusCode.NOT_FOUND_404)
|
||||
return html
|
||||
}
|
||||
|
||||
return this.buildVideoHTML({
|
||||
html,
|
||||
video,
|
||||
addEmbedInfo: true,
|
||||
addOG: true,
|
||||
addTwitterCard: true
|
||||
})
|
||||
}
|
||||
|
||||
@Memoize({ maxAge: MEMOIZE_TTL.EMBED_HTML })
|
||||
static async getEmbedVideoHTML (videoIdArg: string) {
|
||||
const videoId = toCompleteUUID(videoIdArg)
|
||||
|
||||
const videoPromise: Promise<MVideoThumbnailBlacklist> = validator.default.isInt(videoId) || validator.default.isUUID(videoId, 4)
|
||||
? VideoModel.loadWithBlacklist(videoId)
|
||||
: Promise.resolve(undefined)
|
||||
|
||||
const [ html, video ] = await Promise.all([ PageHtml.getEmbedHTML(), videoPromise ])
|
||||
|
||||
if (!video || isVideoInPrivateDirectory(video.privacy) || video.VideoBlacklist) {
|
||||
return CommonEmbedHtml.buildEmptyEmbedHTML({ html, video })
|
||||
}
|
||||
|
||||
return this.buildVideoHTML({
|
||||
html,
|
||||
video,
|
||||
addEmbedInfo: false,
|
||||
addOG: false,
|
||||
addTwitterCard: false
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private static buildVideoHTML (options: {
|
||||
html: string
|
||||
video: MVideo
|
||||
|
||||
addOG: boolean
|
||||
addTwitterCard: boolean
|
||||
addEmbedInfo: boolean
|
||||
}) {
|
||||
const { html, video, addEmbedInfo, addOG, addTwitterCard } = options
|
||||
const escapedTruncatedDescription = TagsHtml.buildEscapedTruncatedDescription(video.description)
|
||||
|
||||
let customHTML = TagsHtml.addTitleTag(html, video.name)
|
||||
customHTML = TagsHtml.addDescriptionTag(customHTML, escapedTruncatedDescription)
|
||||
|
||||
const embed = addEmbedInfo
|
||||
? {
|
||||
url: WEBSERVER.URL + video.getEmbedStaticPath(),
|
||||
createdAt: video.createdAt.toISOString(),
|
||||
duration: getActivityStreamDuration(video.duration),
|
||||
views: video.views
|
||||
}
|
||||
: undefined
|
||||
|
||||
const ogType = addOG
|
||||
? 'video' as 'video'
|
||||
: undefined
|
||||
|
||||
let twitterCard: 'player' | 'summary_large_image'
|
||||
if (addTwitterCard) {
|
||||
twitterCard = CONFIG.SERVICES.TWITTER.WHITELISTED
|
||||
? 'player'
|
||||
: 'summary_large_image'
|
||||
}
|
||||
|
||||
const schemaType = 'VideoObject'
|
||||
|
||||
return TagsHtml.addTags(customHTML, {
|
||||
url: WEBSERVER.URL + video.getWatchStaticPath(),
|
||||
escapedSiteName: escapeHTML(CONFIG.INSTANCE.NAME),
|
||||
escapedTitle: escapeHTML(video.name),
|
||||
escapedTruncatedDescription,
|
||||
|
||||
indexationPolicy: video.remote || video.privacy !== VideoPrivacy.PUBLIC
|
||||
? 'never'
|
||||
: 'always',
|
||||
|
||||
image: { url: WEBSERVER.URL + video.getPreviewStaticPath() },
|
||||
|
||||
embed,
|
||||
ogType,
|
||||
twitterCard,
|
||||
schemaType
|
||||
}, { video })
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue