+ 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(HttpStatusCode.NOT_FOUND_404)
+ return html
+ }
+
+ const description = mdToOneLinePlainText(videoPlaylist.description)
+
+ let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name)
+ customHtml = ClientHtml.addDescriptionTag(customHtml, description)
+
+ const url = WEBSERVER.URL + videoPlaylist.getWatchStaticPath()
+ const originUrl = videoPlaylist.url
+ const title = videoPlaylist.name
+ const siteName = CONFIG.INSTANCE.NAME
+
+ const image = {
+ url: videoPlaylist.getThumbnailUrl()
+ }
+
+ const embed = {
+ url: WEBSERVER.URL + videoPlaylist.getEmbedStaticPath(),
+ createdAt: videoPlaylist.createdAt.toISOString()
+ }
+
+ const list = {
+ numberOfItems: videoPlaylist.get('videosLength') as number
+ }
+
+ const ogType = 'video'
+ const twitterCard = CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary'
+ const schemaType = 'ItemList'
+
+ customHtml = ClientHtml.addTags(customHtml, {
+ url,
+ originUrl,
+ escapedSiteName: escapeHTML(siteName),
+ escapedTitle: escapeHTML(title),
+ escapedDescription: escapeHTML(description),
+ disallowIndexation: videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC,
+ embed,
+ image,
+ list,
+ ogType,
+ twitterCard,
+ schemaType
+ })
+
+ return customHtml
+ }
+
+ static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
+ const accountModelPromise = AccountModel.loadByNameWithHost(nameWithHost)
+ return this.getAccountOrChannelHTMLPage(() => accountModelPromise, req, res)
+ }
+
+ static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
+ const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
+ return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
+ }
+
+ static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
+ const [ account, channel ] = await Promise.all([
+ AccountModel.loadByNameWithHost(nameWithHost),
+ VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
+ ])
+
+ return this.getAccountOrChannelHTMLPage(() => Promise.resolve(account || channel), req, res)
+ }
+
+ static async getEmbedHTML () {
+ const path = ClientHtml.getEmbedPath()
+
+ // Disable HTML cache in dev mode because webpack can regenerate JS files
+ if (!isTestOrDevInstance() && ClientHtml.htmlCache[path]) {
+ return ClientHtml.htmlCache[path]
+ }
+
+ const buffer = await readFile(path)
+ const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
+
+ let html = buffer.toString()
+ html = await ClientHtml.addAsyncPluginCSS(html)
+ html = ClientHtml.addCustomCSS(html)
+ html = ClientHtml.addTitleTag(html)
+ html = ClientHtml.addDescriptionTag(html)
+ html = ClientHtml.addServerConfig(html, serverConfig)
+
+ ClientHtml.htmlCache[path] = html
+
+ return html
+ }
+
+ private static async getAccountOrChannelHTMLPage (
+ loader: () => Promise<MAccountActor | MChannelActor>,
+ req: express.Request,
+ res: express.Response
+ ) {
+ const [ html, entity ] = await Promise.all([
+ ClientHtml.getIndexHTML(req, res),
+ loader()
+ ])
+
+ // Let Angular application handle errors
+ if (!entity) {
+ res.status(HttpStatusCode.NOT_FOUND_404)
+ return ClientHtml.getIndexHTML(req, res)
+ }
+
+ const description = mdToOneLinePlainText(entity.description)
+
+ let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName())
+ customHtml = ClientHtml.addDescriptionTag(customHtml, description)
+
+ const url = entity.getLocalUrl()
+ const originUrl = entity.Actor.url
+ const siteName = CONFIG.INSTANCE.NAME
+ const title = entity.getDisplayName()
+
+ const avatar = getBiggestActorImage(entity.Actor.Avatars)
+ const image = {
+ url: ActorImageModel.getImageUrl(avatar),
+ width: avatar?.width,
+ height: avatar?.height
+ }
+
+ const ogType = 'website'
+ const twitterCard = 'summary'
+ const schemaType = 'ProfilePage'
+
+ customHtml = ClientHtml.addTags(customHtml, {
+ url,
+ originUrl,
+ escapedTitle: escapeHTML(title),
+ escapedSiteName: escapeHTML(siteName),
+ escapedDescription: escapeHTML(description),
+ image,
+ ogType,
+ twitterCard,
+ schemaType,
+ disallowIndexation: !entity.Actor.isOwned()
+ })