Improve channel and account SEO

pull/1697/head
Chocobozzz 2019-02-21 14:06:10 +01:00
parent 84c7cde6e8
commit 92bf2f6299
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 90 additions and 29 deletions

View File

@ -14,7 +14,7 @@
<!-- title tag -->
<!-- description tag -->
<!-- custom css tag -->
<!-- open graph and oembed tags -->
<!-- meta tags -->
<!-- /!\ Do not remove it /!\ -->

View File

@ -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')

View File

@ -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) {

View File

@ -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)
}

View File

@ -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 -->'
}
// ---------------------------------------------------------------------------

View File

@ -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)
}
}

View File

@ -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: {

View File

@ -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: [