diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-21 14:06:10 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-02-21 14:06:10 +0100 |
commit | 92bf2f62995bbaa0402cb4657473ad8d5b6fcf8d (patch) | |
tree | 7f3f34b1503fd21db7f0b913c3b908f004015011 /server | |
parent | 84c7cde6e81426a42e7aa29187b473bc89f1c8f6 (diff) | |
download | PeerTube-92bf2f62995bbaa0402cb4657473ad8d5b6fcf8d.tar.gz PeerTube-92bf2f62995bbaa0402cb4657473ad8d5b6fcf8d.tar.zst PeerTube-92bf2f62995bbaa0402cb4657473ad8d5b6fcf8d.zip |
Improve channel and account SEO
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/client.ts | 14 | ||||
-rw-r--r-- | server/helpers/custom-validators/accounts.ts | 8 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-channels.ts | 6 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/lib/client-html.ts | 67 | ||||
-rw-r--r-- | server/models/account/account.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 10 |
7 files changed, 89 insertions, 28 deletions
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index f17f2a5d2..ece2f460c 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -17,6 +17,8 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html') | |||
17 | // Special route that add OpenGraph and oEmbed tags | 17 | // Special route that add OpenGraph and oEmbed tags |
18 | // Do not use a template engine for a so little thing | 18 | // Do not use a template engine for a so little thing |
19 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) | 19 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) |
20 | clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage)) | ||
21 | clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage)) | ||
20 | 22 | ||
21 | clientsRouter.use( | 23 | clientsRouter.use( |
22 | '/videos/embed', | 24 | '/videos/embed', |
@@ -99,6 +101,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons | |||
99 | return sendHTML(html, res) | 101 | return sendHTML(html, res) |
100 | } | 102 | } |
101 | 103 | ||
104 | async function generateAccountHtmlPage (req: express.Request, res: express.Response) { | ||
105 | const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res) | ||
106 | |||
107 | return sendHTML(html, res) | ||
108 | } | ||
109 | |||
110 | async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) { | ||
111 | const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res) | ||
112 | |||
113 | return sendHTML(html, res) | ||
114 | } | ||
115 | |||
102 | function sendHTML (html: string, res: express.Response) { | 116 | function sendHTML (html: string, res: express.Response) { |
103 | res.set('Content-Type', 'text/html; charset=UTF-8') | 117 | res.set('Content-Type', 'text/html; charset=UTF-8') |
104 | 118 | ||
diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts index 191de1496..aad04fe93 100644 --- a/server/helpers/custom-validators/accounts.ts +++ b/server/helpers/custom-validators/accounts.ts | |||
@@ -38,13 +38,7 @@ function isLocalAccountNameExist (name: string, res: Response, sendNotFound = tr | |||
38 | } | 38 | } |
39 | 39 | ||
40 | function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { | 40 | function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { |
41 | const [ accountName, host ] = nameWithDomain.split('@') | 41 | return isAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound) |
42 | |||
43 | let promise: Bluebird<AccountModel> | ||
44 | if (!host || host === CONFIG.WEBSERVER.HOST) promise = AccountModel.loadLocalByName(accountName) | ||
45 | else promise = AccountModel.loadByNameAndHost(accountName, host) | ||
46 | |||
47 | return isAccountExist(promise, res, sendNotFound) | ||
48 | } | 42 | } |
49 | 43 | ||
50 | async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { | 44 | async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { |
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts index f13519c1d..cbf150e53 100644 --- a/server/helpers/custom-validators/video-channels.ts +++ b/server/helpers/custom-validators/video-channels.ts | |||
@@ -38,11 +38,7 @@ async function isVideoChannelIdExist (id: string, res: express.Response) { | |||
38 | } | 38 | } |
39 | 39 | ||
40 | async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { | 40 | async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { |
41 | const [ name, host ] = nameWithDomain.split('@') | 41 | const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain) |
42 | let videoChannel: VideoChannelModel | ||
43 | |||
44 | if (!host || host === CONFIG.WEBSERVER.HOST) videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | ||
45 | else videoChannel = await VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | ||
46 | 42 | ||
47 | return processVideoChannelExist(videoChannel, res) | 43 | return processVideoChannelExist(videoChannel, res) |
48 | } | 44 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index bb2c6765f..0ede45620 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -661,7 +661,7 @@ const CUSTOM_HTML_TAG_COMMENTS = { | |||
661 | TITLE: '<!-- title tag -->', | 661 | TITLE: '<!-- title tag -->', |
662 | DESCRIPTION: '<!-- description tag -->', | 662 | DESCRIPTION: '<!-- description tag -->', |
663 | CUSTOM_CSS: '<!-- custom css tag -->', | 663 | CUSTOM_CSS: '<!-- custom css tag -->', |
664 | OPENGRAPH_AND_OEMBED: '<!-- open graph and oembed tags -->' | 664 | META_TAGS: '<!-- meta tags -->' |
665 | } | 665 | } |
666 | 666 | ||
667 | // --------------------------------------------------------------------------- | 667 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index b2c376e20..217f6a437 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Bluebird from 'bluebird' | ||
3 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' | 2 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' |
4 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' | 3 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' |
5 | import { join } from 'path' | 4 | import { join } from 'path' |
@@ -9,10 +8,13 @@ import * as validator from 'validator' | |||
9 | import { VideoPrivacy } from '../../shared/models/videos' | 8 | import { VideoPrivacy } from '../../shared/models/videos' |
10 | import { readFile } from 'fs-extra' | 9 | import { readFile } from 'fs-extra' |
11 | import { getActivityStreamDuration } from '../models/video/video-format-utils' | 10 | import { getActivityStreamDuration } from '../models/video/video-format-utils' |
11 | import { AccountModel } from '../models/account/account' | ||
12 | import { VideoChannelModel } from '../models/video/video-channel' | ||
13 | import * as Bluebird from 'bluebird' | ||
12 | 14 | ||
13 | export class ClientHtml { | 15 | export class ClientHtml { |
14 | 16 | ||
15 | private static htmlCache: { [path: string]: string } = {} | 17 | private static htmlCache: { [ path: string ]: string } = {} |
16 | 18 | ||
17 | static invalidCache () { | 19 | static invalidCache () { |
18 | ClientHtml.htmlCache = {} | 20 | ClientHtml.htmlCache = {} |
@@ -28,18 +30,14 @@ export class ClientHtml { | |||
28 | } | 30 | } |
29 | 31 | ||
30 | static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { | 32 | static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { |
31 | let videoPromise: Bluebird<VideoModel> | ||
32 | |||
33 | // Let Angular application handle errors | 33 | // Let Angular application handle errors |
34 | if (validator.isInt(videoId) || validator.isUUID(videoId, 4)) { | 34 | if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) { |
35 | videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
36 | } else { | ||
37 | return ClientHtml.getIndexHTML(req, res) | 35 | return ClientHtml.getIndexHTML(req, res) |
38 | } | 36 | } |
39 | 37 | ||
40 | const [ html, video ] = await Promise.all([ | 38 | const [ html, video ] = await Promise.all([ |
41 | ClientHtml.getIndexHTML(req, res), | 39 | ClientHtml.getIndexHTML(req, res), |
42 | videoPromise | 40 | VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) |
43 | ]) | 41 | ]) |
44 | 42 | ||
45 | // Let Angular application handle errors | 43 | // Let Angular application handle errors |
@@ -49,14 +47,44 @@ export class ClientHtml { | |||
49 | 47 | ||
50 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) | 48 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) |
51 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description)) | 49 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description)) |
52 | customHtml = ClientHtml.addOpenGraphAndOEmbedTags(customHtml, video) | 50 | customHtml = ClientHtml.addVideoOpenGraphAndOEmbedTags(customHtml, video) |
51 | |||
52 | return customHtml | ||
53 | } | ||
54 | |||
55 | static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { | ||
56 | return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res) | ||
57 | } | ||
58 | |||
59 | static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { | ||
60 | return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res) | ||
61 | } | ||
62 | |||
63 | private static async getAccountOrChannelHTMLPage ( | ||
64 | loader: () => Bluebird<AccountModel | VideoChannelModel>, | ||
65 | req: express.Request, | ||
66 | res: express.Response | ||
67 | ) { | ||
68 | const [ html, entity ] = await Promise.all([ | ||
69 | ClientHtml.getIndexHTML(req, res), | ||
70 | loader() | ||
71 | ]) | ||
72 | |||
73 | // Let Angular application handle errors | ||
74 | if (!entity) { | ||
75 | return ClientHtml.getIndexHTML(req, res) | ||
76 | } | ||
77 | |||
78 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName())) | ||
79 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(entity.description)) | ||
80 | customHtml = ClientHtml.addAccountOrChannelMetaTags(customHtml, entity) | ||
53 | 81 | ||
54 | return customHtml | 82 | return customHtml |
55 | } | 83 | } |
56 | 84 | ||
57 | private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { | 85 | private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { |
58 | const path = ClientHtml.getIndexPath(req, res, paramLang) | 86 | const path = ClientHtml.getIndexPath(req, res, paramLang) |
59 | if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | 87 | if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ] |
60 | 88 | ||
61 | const buffer = await readFile(path) | 89 | const buffer = await readFile(path) |
62 | 90 | ||
@@ -64,7 +92,7 @@ export class ClientHtml { | |||
64 | 92 | ||
65 | html = ClientHtml.addCustomCSS(html) | 93 | html = ClientHtml.addCustomCSS(html) |
66 | 94 | ||
67 | ClientHtml.htmlCache[path] = html | 95 | ClientHtml.htmlCache[ path ] = html |
68 | 96 | ||
69 | return html | 97 | return html |
70 | } | 98 | } |
@@ -114,7 +142,7 @@ export class ClientHtml { | |||
114 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) | 142 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) |
115 | } | 143 | } |
116 | 144 | ||
117 | private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 145 | private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { |
118 | const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath() | 146 | const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath() |
119 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() | 147 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() |
120 | 148 | ||
@@ -174,7 +202,7 @@ export class ClientHtml { | |||
174 | 202 | ||
175 | // Opengraph | 203 | // Opengraph |
176 | Object.keys(openGraphMetaTags).forEach(tagName => { | 204 | Object.keys(openGraphMetaTags).forEach(tagName => { |
177 | const tagValue = openGraphMetaTags[tagName] | 205 | const tagValue = openGraphMetaTags[ tagName ] |
178 | 206 | ||
179 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 207 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` |
180 | }) | 208 | }) |
@@ -190,6 +218,17 @@ export class ClientHtml { | |||
190 | // SEO, use origin video url so Google does not index remote videos | 218 | // SEO, use origin video url so Google does not index remote videos |
191 | tagsString += `<link rel="canonical" href="${video.url}" />` | 219 | tagsString += `<link rel="canonical" href="${video.url}" />` |
192 | 220 | ||
193 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) | 221 | return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString) |
222 | } | ||
223 | |||
224 | private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: AccountModel | VideoChannelModel) { | ||
225 | // SEO, use origin account or channel URL | ||
226 | const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />` | ||
227 | |||
228 | return this.addOpenGraphAndOEmbedTags(htmlStringPage, metaTags) | ||
229 | } | ||
230 | |||
231 | private static addOpenGraphAndOEmbedTags (htmlStringPage: string, metaTags: string) { | ||
232 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, metaTags) | ||
194 | } | 233 | } |
195 | } | 234 | } |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 84ef0b30d..747b51afb 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -24,6 +24,8 @@ import { getSort, throwIfNotValid } from '../utils' | |||
24 | import { VideoChannelModel } from '../video/video-channel' | 24 | import { VideoChannelModel } from '../video/video-channel' |
25 | import { VideoCommentModel } from '../video/video-comment' | 25 | import { VideoCommentModel } from '../video/video-comment' |
26 | import { UserModel } from './user' | 26 | import { UserModel } from './user' |
27 | import * as Bluebird from '../../helpers/custom-validators/accounts' | ||
28 | import { CONFIG } from '../../initializers' | ||
27 | 29 | ||
28 | @DefaultScope({ | 30 | @DefaultScope({ |
29 | include: [ | 31 | include: [ |
@@ -153,6 +155,14 @@ export class AccountModel extends Model<AccountModel> { | |||
153 | return AccountModel.findOne(query) | 155 | return AccountModel.findOne(query) |
154 | } | 156 | } |
155 | 157 | ||
158 | static loadByNameWithHost (nameWithHost: string) { | ||
159 | const [ accountName, host ] = nameWithHost.split('@') | ||
160 | |||
161 | if (!host || host === CONFIG.WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName) | ||
162 | |||
163 | return AccountModel.loadByNameAndHost(accountName, host) | ||
164 | } | ||
165 | |||
156 | static loadLocalByName (name: string) { | 166 | static loadLocalByName (name: string) { |
157 | const query = { | 167 | const query = { |
158 | where: { | 168 | where: { |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 5598d80f6..91dd0440c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -28,7 +28,7 @@ import { AccountModel } from '../account/account' | |||
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 29 | import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
30 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
31 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 31 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
32 | import { ServerModel } from '../server/server' | 32 | import { ServerModel } from '../server/server' |
33 | import { DefineIndexesOptions } from 'sequelize' | 33 | import { DefineIndexesOptions } from 'sequelize' |
34 | 34 | ||
@@ -378,6 +378,14 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
378 | .findOne(query) | 378 | .findOne(query) |
379 | } | 379 | } |
380 | 380 | ||
381 | static loadByNameWithHostAndPopulateAccount (nameWithHost: string) { | ||
382 | const [ name, host ] = nameWithHost.split('@') | ||
383 | |||
384 | if (!host || host === CONFIG.WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | ||
385 | |||
386 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | ||
387 | } | ||
388 | |||
381 | static loadLocalByNameAndPopulateAccount (name: string) { | 389 | static loadLocalByNameAndPopulateAccount (name: string) { |
382 | const query = { | 390 | const query = { |
383 | include: [ | 391 | include: [ |