Improve channel and account SEO
authorChocobozzz <me@florianbigard.com>
Thu, 21 Feb 2019 13:06:10 +0000 (14:06 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 21 Feb 2019 13:06:10 +0000 (14:06 +0100)
client/src/index.html
server/controllers/client.ts
server/helpers/custom-validators/accounts.ts
server/helpers/custom-validators/video-channels.ts
server/initializers/constants.ts
server/lib/client-html.ts
server/models/account/account.ts
server/models/video/video-channel.ts

index 8c257824e9b0349a6757e03b2bd6c41e2ddf4d4c..6aa885eb7d4e683e7bf312b04982a1e22b3467da 100644 (file)
@@ -14,7 +14,7 @@
       <!-- title tag -->
       <!-- description tag -->
       <!-- custom css tag -->
-      <!-- open graph and oembed tags -->
+      <!-- meta tags -->
 
     <!-- /!\ Do not remove it /!\ -->
 
index f17f2a5d29cb0d1bc4e56aeb5a74706742ed4a04..ece2f460cc210cbe3d1830ddac57c6eae7c620b4 100644 (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')
 
index 191de1496eec4b545e04a0438a1b85f95881b986..aad04fe934ec2fd511cc45086b5cadee4b001959 100644 (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) {
index f13519c1dd0de4fe1604d482e83da2fdadda541b..cbf150e53d29bd85ef37912cdcf918fb1c4e5731 100644 (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)
 }
index bb2c6765f27fb62e7ea50e8b4bfd8f505d3afd6c..0ede456201f2321935eb507c64a932e2fd851612 100644 (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 -->'
 }
 
 // ---------------------------------------------------------------------------
index b2c376e209529de65740dde6003ae768e256a259..217f6a437ab65f0ebdae3be6fd89dcd869a52c4c 100644 (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)
   }
 }
index 84ef0b30d7a96aacd2b21b275e6aada2038858ea..747b51afbb5e58fbe5a146193c370f5fdc3b2641 100644 (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: {
index 5598d80f615fe53f4ede030caf28b251ab55c42c..91dd0440ce227c3cad43c44a3e7c1c37daeff6ad 100644 (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: [