diff options
author | Chocobozzz <me@florianbigard.com> | 2018-07-18 09:52:46 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-07-18 10:00:37 +0200 |
commit | e032aec9b92be25a996923361f83a96a89505254 (patch) | |
tree | 245b559061fdcb1c27946333ff7ecd6bd82247f7 /server/controllers/client.ts | |
parent | 1d94c154689b89b2c5e55f6e12ec25f49b369d52 (diff) | |
download | PeerTube-e032aec9b92be25a996923361f83a96a89505254.tar.gz PeerTube-e032aec9b92be25a996923361f83a96a89505254.tar.zst PeerTube-e032aec9b92be25a996923361f83a96a89505254.zip |
Render CSS/title/description tags on server side
Diffstat (limited to 'server/controllers/client.ts')
-rw-r--r-- | server/controllers/client.ts | 150 |
1 files changed, 14 insertions, 136 deletions
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 13ca15e9d..352d45fbf 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -1,21 +1,10 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as express from 'express' | 1 | import * as express from 'express' |
3 | import * as helmet from 'helmet' | ||
4 | import { join } from 'path' | 2 | import { join } from 'path' |
5 | import * as validator from 'validator' | 3 | import { root } from '../helpers/core-utils' |
6 | import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' | 4 | import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers' |
7 | import { ACCEPT_HEADERS, CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' | ||
8 | import { asyncMiddleware } from '../middlewares' | 5 | import { asyncMiddleware } from '../middlewares' |
9 | import { VideoModel } from '../models/video/video' | 6 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' |
10 | import { VideoPrivacy } from '../../shared/models/videos' | 7 | import { ClientHtml } from '../lib/client-html' |
11 | import { | ||
12 | buildFileLocale, | ||
13 | getCompleteLocale, | ||
14 | getDefaultLocale, | ||
15 | is18nLocale, | ||
16 | LOCALE_FILES, | ||
17 | POSSIBLE_LOCALES | ||
18 | } from '../../shared/models/i18n/i18n' | ||
19 | 8 | ||
20 | const clientsRouter = express.Router() | 9 | const clientsRouter = express.Router() |
21 | 10 | ||
@@ -79,7 +68,7 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex | |||
79 | // Try to provide the right language index.html | 68 | // Try to provide the right language index.html |
80 | clientsRouter.use('/(:language)?', function (req, res) { | 69 | clientsRouter.use('/(:language)?', function (req, res) { |
81 | if (req.accepts(ACCEPT_HEADERS) === 'html') { | 70 | if (req.accepts(ACCEPT_HEADERS) === 'html') { |
82 | return res.sendFile(getIndexPath(req, res, req.params.language)) | 71 | return generateHTMLPage(req, res, req.params.language) |
83 | } | 72 | } |
84 | 73 | ||
85 | return res.status(404).end() | 74 | return res.status(404).end() |
@@ -93,131 +82,20 @@ export { | |||
93 | 82 | ||
94 | // --------------------------------------------------------------------------- | 83 | // --------------------------------------------------------------------------- |
95 | 84 | ||
96 | function getIndexPath (req: express.Request, res: express.Response, paramLang?: string) { | 85 | async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { |
97 | let lang: string | 86 | const html = await ClientHtml.getIndexHTML(req, res) |
98 | 87 | ||
99 | // Check param lang validity | 88 | return sendHTML(html, res) |
100 | if (paramLang && is18nLocale(paramLang)) { | ||
101 | lang = paramLang | ||
102 | |||
103 | // Save locale in cookies | ||
104 | res.cookie('clientLanguage', lang, { | ||
105 | secure: CONFIG.WEBSERVER.SCHEME === 'https', | ||
106 | sameSite: true, | ||
107 | maxAge: 1000 * 3600 * 24 * 90 // 3 months | ||
108 | }) | ||
109 | |||
110 | } else if (req.cookies.clientLanguage && is18nLocale(req.cookies.clientLanguage)) { | ||
111 | lang = req.cookies.clientLanguage | ||
112 | } else { | ||
113 | lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale() | ||
114 | } | ||
115 | |||
116 | return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html') | ||
117 | } | 89 | } |
118 | 90 | ||
119 | function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 91 | async function generateWatchHtmlPage (req: express.Request, res: express.Response) { |
120 | const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName() | 92 | const html = await ClientHtml.getWatchHTMLPage(req.params.id + '', req, res) |
121 | const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
122 | |||
123 | const videoNameEscaped = escapeHTML(video.name) | ||
124 | const videoDescriptionEscaped = escapeHTML(video.description) | ||
125 | const embedUrl = CONFIG.WEBSERVER.URL + video.getEmbedStaticPath() | ||
126 | |||
127 | const openGraphMetaTags = { | ||
128 | 'og:type': 'video', | ||
129 | 'og:title': videoNameEscaped, | ||
130 | 'og:image': previewUrl, | ||
131 | 'og:url': videoUrl, | ||
132 | 'og:description': videoDescriptionEscaped, | ||
133 | |||
134 | 'og:video:url': embedUrl, | ||
135 | 'og:video:secure_url': embedUrl, | ||
136 | 'og:video:type': 'text/html', | ||
137 | 'og:video:width': EMBED_SIZE.width, | ||
138 | 'og:video:height': EMBED_SIZE.height, | ||
139 | |||
140 | 'name': videoNameEscaped, | ||
141 | 'description': videoDescriptionEscaped, | ||
142 | 'image': previewUrl, | ||
143 | |||
144 | 'twitter:card': CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary_large_image', | ||
145 | 'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME, | ||
146 | 'twitter:title': videoNameEscaped, | ||
147 | 'twitter:description': videoDescriptionEscaped, | ||
148 | 'twitter:image': previewUrl, | ||
149 | 'twitter:player': embedUrl, | ||
150 | 'twitter:player:width': EMBED_SIZE.width, | ||
151 | 'twitter:player:height': EMBED_SIZE.height | ||
152 | } | ||
153 | |||
154 | const oembedLinkTags = [ | ||
155 | { | ||
156 | type: 'application/json+oembed', | ||
157 | href: CONFIG.WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(videoUrl), | ||
158 | title: videoNameEscaped | ||
159 | } | ||
160 | ] | ||
161 | |||
162 | const schemaTags = { | ||
163 | '@context': 'http://schema.org', | ||
164 | '@type': 'VideoObject', | ||
165 | name: videoNameEscaped, | ||
166 | description: videoDescriptionEscaped, | ||
167 | thumbnailUrl: previewUrl, | ||
168 | uploadDate: video.createdAt.toISOString(), | ||
169 | duration: video.getActivityStreamDuration(), | ||
170 | contentUrl: videoUrl, | ||
171 | embedUrl: embedUrl, | ||
172 | interactionCount: video.views | ||
173 | } | ||
174 | |||
175 | let tagsString = '' | ||
176 | |||
177 | // Opengraph | ||
178 | Object.keys(openGraphMetaTags).forEach(tagName => { | ||
179 | const tagValue = openGraphMetaTags[tagName] | ||
180 | 93 | ||
181 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 94 | return sendHTML(html, res) |
182 | }) | ||
183 | |||
184 | // OEmbed | ||
185 | for (const oembedLinkTag of oembedLinkTags) { | ||
186 | tagsString += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.title}" />` | ||
187 | } | ||
188 | |||
189 | // Schema.org | ||
190 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` | ||
191 | |||
192 | // SEO | ||
193 | tagsString += `<link rel="canonical" href="${videoUrl}" />` | ||
194 | |||
195 | return htmlStringPage.replace(OPENGRAPH_AND_OEMBED_COMMENT, tagsString) | ||
196 | } | 95 | } |
197 | 96 | ||
198 | async function generateWatchHtmlPage (req: express.Request, res: express.Response, next: express.NextFunction) { | 97 | function sendHTML (html: string, res: express.Response) { |
199 | const videoId = '' + req.params.id | 98 | res.set('Content-Type', 'text/html; charset=UTF-8') |
200 | let videoPromise: Bluebird<VideoModel> | ||
201 | |||
202 | // Let Angular application handle errors | ||
203 | if (validator.isUUID(videoId, 4)) { | ||
204 | videoPromise = VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(videoId) | ||
205 | } else if (validator.isInt(videoId)) { | ||
206 | videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId) | ||
207 | } else { | ||
208 | return res.sendFile(getIndexPath(req, res)) | ||
209 | } | ||
210 | |||
211 | let [ file, video ] = await Promise.all([ | ||
212 | readFileBufferPromise(getIndexPath(req, res)), | ||
213 | videoPromise | ||
214 | ]) | ||
215 | |||
216 | const html = file.toString() | ||
217 | |||
218 | // Let Angular application handle errors | ||
219 | if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req, res)) | ||
220 | 99 | ||
221 | const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video) | 100 | return res.send(html) |
222 | res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) | ||
223 | } | 101 | } |