Improve channel and account SEO
This commit is contained in:
parent
84c7cde6e8
commit
92bf2f6299
8 changed files with 90 additions and 29 deletions
|
@ -14,7 +14,7 @@
|
|||
<!-- title tag -->
|
||||
<!-- description tag -->
|
||||
<!-- custom css tag -->
|
||||
<!-- open graph and oembed tags -->
|
||||
<!-- meta tags -->
|
||||
|
||||
<!-- /!\ Do not remove it /!\ -->
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
|
|||
// Special route that add OpenGraph and oEmbed tags
|
||||
// Do not use a template engine for a so little thing
|
||||
clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
|
||||
clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
|
||||
clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
|
||||
|
||||
clientsRouter.use(
|
||||
'/videos/embed',
|
||||
|
@ -99,6 +101,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
|
|||
return sendHTML(html, res)
|
||||
}
|
||||
|
||||
async function generateAccountHtmlPage (req: express.Request, res: express.Response) {
|
||||
const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res)
|
||||
|
||||
return sendHTML(html, res)
|
||||
}
|
||||
|
||||
async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) {
|
||||
const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res)
|
||||
|
||||
return sendHTML(html, res)
|
||||
}
|
||||
|
||||
function sendHTML (html: string, res: express.Response) {
|
||||
res.set('Content-Type', 'text/html; charset=UTF-8')
|
||||
|
||||
|
|
|
@ -38,13 +38,7 @@ function isLocalAccountNameExist (name: string, res: Response, sendNotFound = tr
|
|||
}
|
||||
|
||||
function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
|
||||
const [ accountName, host ] = nameWithDomain.split('@')
|
||||
|
||||
let promise: Bluebird<AccountModel>
|
||||
if (!host || host === CONFIG.WEBSERVER.HOST) promise = AccountModel.loadLocalByName(accountName)
|
||||
else promise = AccountModel.loadByNameAndHost(accountName, host)
|
||||
|
||||
return isAccountExist(promise, res, sendNotFound)
|
||||
return isAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound)
|
||||
}
|
||||
|
||||
async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) {
|
||||
|
|
|
@ -38,11 +38,7 @@ async function isVideoChannelIdExist (id: string, res: express.Response) {
|
|||
}
|
||||
|
||||
async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
|
||||
const [ name, host ] = nameWithDomain.split('@')
|
||||
let videoChannel: VideoChannelModel
|
||||
|
||||
if (!host || host === CONFIG.WEBSERVER.HOST) videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
|
||||
else videoChannel = await VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
|
||||
const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
|
||||
|
||||
return processVideoChannelExist(videoChannel, res)
|
||||
}
|
||||
|
|
|
@ -661,7 +661,7 @@ const CUSTOM_HTML_TAG_COMMENTS = {
|
|||
TITLE: '<!-- title tag -->',
|
||||
DESCRIPTION: '<!-- description tag -->',
|
||||
CUSTOM_CSS: '<!-- custom css tag -->',
|
||||
OPENGRAPH_AND_OEMBED: '<!-- open graph and oembed tags -->'
|
||||
META_TAGS: '<!-- meta tags -->'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as express from 'express'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
|
||||
import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers'
|
||||
import { join } from 'path'
|
||||
|
@ -9,10 +8,13 @@ import * as validator from 'validator'
|
|||
import { VideoPrivacy } from '../../shared/models/videos'
|
||||
import { readFile } from 'fs-extra'
|
||||
import { getActivityStreamDuration } from '../models/video/video-format-utils'
|
||||
import { AccountModel } from '../models/account/account'
|
||||
import { VideoChannelModel } from '../models/video/video-channel'
|
||||
import * as Bluebird from 'bluebird'
|
||||
|
||||
export class ClientHtml {
|
||||
|
||||
private static htmlCache: { [path: string]: string } = {}
|
||||
private static htmlCache: { [ path: string ]: string } = {}
|
||||
|
||||
static invalidCache () {
|
||||
ClientHtml.htmlCache = {}
|
||||
|
@ -28,18 +30,14 @@ export class ClientHtml {
|
|||
}
|
||||
|
||||
static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) {
|
||||
let videoPromise: Bluebird<VideoModel>
|
||||
|
||||
// Let Angular application handle errors
|
||||
if (validator.isInt(videoId) || validator.isUUID(videoId, 4)) {
|
||||
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
||||
} else {
|
||||
if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) {
|
||||
return ClientHtml.getIndexHTML(req, res)
|
||||
}
|
||||
|
||||
const [ html, video ] = await Promise.all([
|
||||
ClientHtml.getIndexHTML(req, res),
|
||||
videoPromise
|
||||
VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
||||
])
|
||||
|
||||
// Let Angular application handle errors
|
||||
|
@ -49,14 +47,44 @@ export class ClientHtml {
|
|||
|
||||
let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name))
|
||||
customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description))
|
||||
customHtml = ClientHtml.addOpenGraphAndOEmbedTags(customHtml, video)
|
||||
customHtml = ClientHtml.addVideoOpenGraphAndOEmbedTags(customHtml, video)
|
||||
|
||||
return customHtml
|
||||
}
|
||||
|
||||
static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
|
||||
return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res)
|
||||
}
|
||||
|
||||
static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
|
||||
return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res)
|
||||
}
|
||||
|
||||
private static async getAccountOrChannelHTMLPage (
|
||||
loader: () => Bluebird<AccountModel | VideoChannelModel>,
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) {
|
||||
const [ html, entity ] = await Promise.all([
|
||||
ClientHtml.getIndexHTML(req, res),
|
||||
loader()
|
||||
])
|
||||
|
||||
// Let Angular application handle errors
|
||||
if (!entity) {
|
||||
return ClientHtml.getIndexHTML(req, res)
|
||||
}
|
||||
|
||||
let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName()))
|
||||
customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(entity.description))
|
||||
customHtml = ClientHtml.addAccountOrChannelMetaTags(customHtml, entity)
|
||||
|
||||
return customHtml
|
||||
}
|
||||
|
||||
private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) {
|
||||
const path = ClientHtml.getIndexPath(req, res, paramLang)
|
||||
if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
|
||||
if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ]
|
||||
|
||||
const buffer = await readFile(path)
|
||||
|
||||
|
@ -64,7 +92,7 @@ export class ClientHtml {
|
|||
|
||||
html = ClientHtml.addCustomCSS(html)
|
||||
|
||||
ClientHtml.htmlCache[path] = html
|
||||
ClientHtml.htmlCache[ path ] = html
|
||||
|
||||
return html
|
||||
}
|
||||
|
@ -114,7 +142,7 @@ export class ClientHtml {
|
|||
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
|
||||
}
|
||||
|
||||
private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
|
||||
private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
|
||||
const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath()
|
||||
const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
|
||||
|
||||
|
@ -174,7 +202,7 @@ export class ClientHtml {
|
|||
|
||||
// Opengraph
|
||||
Object.keys(openGraphMetaTags).forEach(tagName => {
|
||||
const tagValue = openGraphMetaTags[tagName]
|
||||
const tagValue = openGraphMetaTags[ tagName ]
|
||||
|
||||
tagsString += `<meta property="${tagName}" content="${tagValue}" />`
|
||||
})
|
||||
|
@ -190,6 +218,17 @@ export class ClientHtml {
|
|||
// SEO, use origin video url so Google does not index remote videos
|
||||
tagsString += `<link rel="canonical" href="${video.url}" />`
|
||||
|
||||
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString)
|
||||
return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
|
||||
}
|
||||
|
||||
private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: AccountModel | VideoChannelModel) {
|
||||
// SEO, use origin account or channel URL
|
||||
const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />`
|
||||
|
||||
return this.addOpenGraphAndOEmbedTags(htmlStringPage, metaTags)
|
||||
}
|
||||
|
||||
private static addOpenGraphAndOEmbedTags (htmlStringPage: string, metaTags: string) {
|
||||
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, metaTags)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import { getSort, throwIfNotValid } from '../utils'
|
|||
import { VideoChannelModel } from '../video/video-channel'
|
||||
import { VideoCommentModel } from '../video/video-comment'
|
||||
import { UserModel } from './user'
|
||||
import * as Bluebird from '../../helpers/custom-validators/accounts'
|
||||
import { CONFIG } from '../../initializers'
|
||||
|
||||
@DefaultScope({
|
||||
include: [
|
||||
|
@ -153,6 +155,14 @@ export class AccountModel extends Model<AccountModel> {
|
|||
return AccountModel.findOne(query)
|
||||
}
|
||||
|
||||
static loadByNameWithHost (nameWithHost: string) {
|
||||
const [ accountName, host ] = nameWithHost.split('@')
|
||||
|
||||
if (!host || host === CONFIG.WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
|
||||
|
||||
return AccountModel.loadByNameAndHost(accountName, host)
|
||||
}
|
||||
|
||||
static loadLocalByName (name: string) {
|
||||
const query = {
|
||||
where: {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { AccountModel } from '../account/account'
|
|||
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
|
||||
import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { ServerModel } from '../server/server'
|
||||
import { DefineIndexesOptions } from 'sequelize'
|
||||
|
||||
|
@ -378,6 +378,14 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
|||
.findOne(query)
|
||||
}
|
||||
|
||||
static loadByNameWithHostAndPopulateAccount (nameWithHost: string) {
|
||||
const [ name, host ] = nameWithHost.split('@')
|
||||
|
||||
if (!host || host === CONFIG.WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
|
||||
|
||||
return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
|
||||
}
|
||||
|
||||
static loadLocalByNameAndPopulateAccount (name: string) {
|
||||
const query = {
|
||||
include: [
|
||||
|
|
Loading…
Reference in a new issue