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