aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-21 14:06:10 +0100
committerChocobozzz <me@florianbigard.com>2019-02-21 14:06:10 +0100
commit92bf2f62995bbaa0402cb4657473ad8d5b6fcf8d (patch)
tree7f3f34b1503fd21db7f0b913c3b908f004015011 /server
parent84c7cde6e81426a42e7aa29187b473bc89f1c8f6 (diff)
downloadPeerTube-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.ts14
-rw-r--r--server/helpers/custom-validators/accounts.ts8
-rw-r--r--server/helpers/custom-validators/video-channels.ts6
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/client-html.ts67
-rw-r--r--server/models/account/account.ts10
-rw-r--r--server/models/video/video-channel.ts10
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
19clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) 19clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
20clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
21clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
20 22
21clientsRouter.use( 23clientsRouter.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
104async 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
110async 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
102function sendHTML (html: string, res: express.Response) { 116function 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
40function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { 40function 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
50async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { 44async 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
40async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { 40async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Bluebird from 'bluebird'
3import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' 2import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
4import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' 3import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers'
5import { join } from 'path' 4import { join } from 'path'
@@ -9,10 +8,13 @@ import * as validator from 'validator'
9import { VideoPrivacy } from '../../shared/models/videos' 8import { VideoPrivacy } from '../../shared/models/videos'
10import { readFile } from 'fs-extra' 9import { readFile } from 'fs-extra'
11import { getActivityStreamDuration } from '../models/video/video-format-utils' 10import { getActivityStreamDuration } from '../models/video/video-format-utils'
11import { AccountModel } from '../models/account/account'
12import { VideoChannelModel } from '../models/video/video-channel'
13import * as Bluebird from 'bluebird'
12 14
13export class ClientHtml { 15export 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'
24import { VideoChannelModel } from '../video/video-channel' 24import { VideoChannelModel } from '../video/video-channel'
25import { VideoCommentModel } from '../video/video-comment' 25import { VideoCommentModel } from '../video/video-comment'
26import { UserModel } from './user' 26import { UserModel } from './user'
27import * as Bluebird from '../../helpers/custom-validators/accounts'
28import { 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'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS } from '../../initializers' 31import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { DefineIndexesOptions } from 'sequelize' 33import { 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: [