Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fcbk open-graph and twitter-card metas for accounts, video-channels, playlists urls #2996

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions server/controllers/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage))
clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
Expand Down Expand Up @@ -134,6 +135,12 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
return sendHTML(html, res)
}

async function generateWatchPlaylistHtmlPage (req: express.Request, res: express.Response) {
const html = await ClientHtml.getWatchPlaylistHTMLPage(req.params.id + '', req, res)

return sendHTML(html, res)
}

async function generateAccountHtmlPage (req: express.Request, res: express.Response) {
const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res)

Expand Down
145 changes: 139 additions & 6 deletions server/lib/client-html.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import * as express from 'express'
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER, FILES_CONTENT_HASH } from '../initializers/constants'
import {
AVATARS_SIZE,
CUSTOM_HTML_TAG_COMMENTS,
EMBED_SIZE,
PLUGIN_GLOBAL_CSS_PATH,
WEBSERVER,
FILES_CONTENT_HASH
} from '../initializers/constants'
import { join } from 'path'
import { escapeHTML, sha256 } from '../helpers/core-utils'
import { VideoModel } from '../models/video/video'
import { VideoPlaylistModel } from '../models/video/video-playlist'
import validator from 'validator'
import { VideoPrivacy } from '../../shared/models/videos'
import { VideoPrivacy, VideoPlaylistPrivacy } 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'
import { CONFIG } from '../initializers/config'
import { logger } from '../helpers/logger'
import { MAccountActor, MChannelActor, MVideo } from '../types/models'
import { MAccountActor, MChannelActor, MVideo, MVideoPlaylist } from '../types/models'

export class ClientHtml {

Expand Down Expand Up @@ -61,6 +69,31 @@ export class ClientHtml {
return customHtml
}

static async getWatchPlaylistHTMLPage (videoPlaylistId: string, req: express.Request, res: express.Response) {
// Let Angular application handle errors
if (!validator.isInt(videoPlaylistId) && !validator.isUUID(videoPlaylistId, 4)) {
res.status(404)
return ClientHtml.getIndexHTML(req, res)
}

const [ html, videoPlaylist ] = await Promise.all([
ClientHtml.getIndexHTML(req, res),
VideoPlaylistModel.loadWithAccountAndChannel(videoPlaylistId, null)
])

// Let Angular application handle errors
if (!videoPlaylist || videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
res.status(404)
return html
}

let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name))
customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(videoPlaylist.description))
customHtml = ClientHtml.addVideoPlaylistOpenGraphAndMetaTags(customHtml, videoPlaylist)

return customHtml
}

static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res)
}
Expand All @@ -87,7 +120,7 @@ export class ClientHtml {

let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName()))
customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(entity.description))
customHtml = ClientHtml.addAccountOrChannelMetaTags(customHtml, entity)
customHtml = ClientHtml.addAccountOrChannelOpenGraphAndMetaTags(customHtml, entity)

return customHtml
}
Expand Down Expand Up @@ -262,9 +295,109 @@ export class ClientHtml {
return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
}

private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: MAccountActor | MChannelActor) {
private static addVideoPlaylistOpenGraphAndMetaTags (htmlStringPage: string, videoPlaylist: MVideoPlaylist) {
const videoPlaylistThumbnailUrl = videoPlaylist.getThumbnailUrl()
const videoPlaylistUrl = videoPlaylist.getWatchUrl()

const videoPlaylistNameEscaped = escapeHTML(videoPlaylist.name)
const videoPlaylistDescriptionEscaped = escapeHTML(videoPlaylist.description)

const openGraphMetaTags = {
'og:title': videoPlaylistNameEscaped,
'og:image': videoPlaylistThumbnailUrl,
'og:url': videoPlaylistUrl,
'og:description': videoPlaylistDescriptionEscaped,

'name': videoPlaylistNameEscaped,
'description': videoPlaylistDescriptionEscaped,
'image': videoPlaylistThumbnailUrl,

'twitter:card': 'summary_large_image',
'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME,
'twitter:title': videoPlaylistNameEscaped,
'twitter:description': videoPlaylistDescriptionEscaped,
'twitter:image': videoPlaylistThumbnailUrl
}

const schemaTags = {
'@context': 'http://schema.org',
'@type': 'ItemList',
'name': videoPlaylistNameEscaped,
'description': videoPlaylistDescriptionEscaped,
'image': videoPlaylistThumbnailUrl,
'numberOfItems': videoPlaylist.get('videosLength')
}

let tagsString = ''

// Opengraph
Object.keys(openGraphMetaTags).forEach(tagName => {
const tagValue = openGraphMetaTags[tagName]

tagsString += `<meta property="${tagName}" content="${tagValue}" />`
})

// Schema.org
tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>`

// SEO, use origin videoPlaylist url so Google does not index remote videos
tagsString += `<link rel="canonical" href="${videoPlaylistUrl}" />`

return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
}

private static addAccountOrChannelOpenGraphAndMetaTags (htmlStringPage: string, entity: MAccountActor | MChannelActor) {
const entityDisplayNameEscaped = escapeHTML(entity.getDisplayName())
const entityDescriptionEscaped = escapeHTML(entity.description)
const entityAvatarUrl = entity.Actor.getAvatarUrl()
const entityUrl = entity.Actor.url

const openGraphMetaTags = {
'og:type': 'website',
'og:title': entityDisplayNameEscaped,
'og:image': entityAvatarUrl,
'og:image:width': AVATARS_SIZE.width,
'og:image:height': AVATARS_SIZE.height,
'og:url': entityUrl,
'og:description': entityDescriptionEscaped,

'name': entityDisplayNameEscaped,
'description': entityDescriptionEscaped,
'image': entityAvatarUrl,

'twitter:card': 'summary',
'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME,
'twitter:title': entityDisplayNameEscaped,
'twitter:description': entityDescriptionEscaped,
'twitter:image': entityAvatarUrl,
'twitter:image:width': AVATARS_SIZE.width,
'twitter:image:height': AVATARS_SIZE.height
}

const schemaTags = {
'@context': 'http://schema.org',
'@type': 'ProfilePage',
'name': entityDisplayNameEscaped,
'description': entityDescriptionEscaped,
'thumbnailUrl': entityAvatarUrl,
'image': entityAvatarUrl,
'url': entityUrl
}

let metaTags = ''

// Opengraph
Object.keys(openGraphMetaTags).forEach(tagName => {
const tagValue = openGraphMetaTags[tagName]

metaTags += `<meta property="${tagName}" content="${tagValue}" />`
})

// Schema.org
metaTags += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>`

// SEO, use origin account or channel URL
const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />`
metaTags += `<link rel="canonical" href="${entityUrl}" />`

return this.addOpenGraphAndOEmbedTags(htmlStringPage, metaTags)
}
Expand Down
4 changes: 4 additions & 0 deletions server/models/video/video-playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
}

getWatchUrl () {
return WEBSERVER.URL + '/videos/watch/playlist/' + this.uuid
}

setAsRefreshed () {
this.changed('updatedAt', true)

Expand Down