feat: add canonical tag to pages with pagination

This commit is contained in:
kontrollanten 2024-09-21 22:41:32 +02:00
parent a91fd0cb35
commit 10662889f3
8 changed files with 172 additions and 11 deletions

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai' import { expect } from 'chai'
import { HttpStatusCode, VideoPlaylistCreateResult } from '@peertube/peertube-models' import { HttpStatusCode, ServerConfig, VideoPlaylistCreateResult } from '@peertube/peertube-models'
import { cleanupTests, makeGetRequest, makeHTMLRequest, PeerTubeServer } from '@peertube/peertube-server-commands' import { cleanupTests, makeGetRequest, makeHTMLRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
import { checkIndexTags, getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js' import { checkIndexTags, getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js'
@ -25,6 +25,8 @@ describe('Test index HTML generation', function () {
shortDescription: string shortDescription: string
} }
const getTitleWithSuffix = (title: string, config: ServerConfig) => `${title} - ${config.instance.name}`
before(async function () { before(async function () {
this.timeout(120000); this.timeout(120000);
@ -49,7 +51,7 @@ describe('Test index HTML generation', function () {
const config = await servers[0].config.getConfig() const config = await servers[0].config.getConfig()
const res = await makeHTMLRequest(servers[0].url, '/videos/trending') const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
checkIndexTags(res.text, instanceConfig.name, instanceConfig.shortDescription, '', config) checkIndexTags(res.text, getTitleWithSuffix('Trending', config), instanceConfig.shortDescription, '', config)
}) })
it('Should update the customized configuration and have the correct index html tags', async function () { it('Should update the customized configuration and have the correct index html tags', async function () {
@ -73,20 +75,25 @@ describe('Test index HTML generation', function () {
const config = await servers[0].config.getConfig() const config = await servers[0].config.getConfig()
const res = await makeHTMLRequest(servers[0].url, '/videos/trending') const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config)
}) })
it('Should have valid index html updated tags (title, description...)', async function () { it('Should have valid index html updated tags (title, description...)', async function () {
const config = await servers[0].config.getConfig() const config = await servers[0].config.getConfig()
const res = await makeHTMLRequest(servers[0].url, '/videos/trending') const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config)
}) })
}) })
describe('Canonical tags', function () { describe('Canonical tags', function () {
it('Should use the original video URL for the canonical tag', async function () { it('Should use the original video URL for the canonical tag', async function () {
const res = await makeHTMLRequest(servers[0].url, '/videos/trending?page=2')
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/trending?page=2" />`)
})
it('Should use pagination in video URL for the canonical tag', async function () {
for (const basePath of getWatchVideoBasePaths()) { for (const basePath of getWatchVideoBasePaths()) {
for (const id of videoIds) { for (const id of videoIds) {
const res = await makeHTMLRequest(servers[0].url, basePath + id) const res = await makeHTMLRequest(servers[0].url, basePath + id)
@ -114,6 +121,18 @@ describe('Test index HTML generation', function () {
accountURLtest(await makeHTMLRequest(servers[0].url, '/@root@' + servers[0].host)) accountURLtest(await makeHTMLRequest(servers[0].url, '/@root@' + servers[0].host))
}) })
it('Should use pagination in account video channels URL for the canonical tag', async function () {
const res = await makeHTMLRequest(servers[0].url, '/a/root/video-channels?page=2')
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root/video-channels?page=2" />`)
})
it('Should use pagination in account videos URL for the canonical tag', async function () {
const res = await makeHTMLRequest(servers[0].url, '/a/root/videos?page=2')
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root/videos?page=2" />`)
})
it('Should use the original channel URL for the canonical tag', async function () { it('Should use the original channel URL for the canonical tag', async function () {
const channelURLtests = res => { const channelURLtests = res => {
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos" />`) expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos" />`)
@ -123,6 +142,18 @@ describe('Test index HTML generation', function () {
channelURLtests(await makeHTMLRequest(servers[0].url, '/c/root_channel@' + servers[0].host)) channelURLtests(await makeHTMLRequest(servers[0].url, '/c/root_channel@' + servers[0].host))
channelURLtests(await makeHTMLRequest(servers[0].url, '/@root_channel@' + servers[0].host)) channelURLtests(await makeHTMLRequest(servers[0].url, '/@root_channel@' + servers[0].host))
}) })
it('Should use pagination in channel videos URL for the canonical tag', async function () {
const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/videos?page=2')
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos?page=2" />`)
})
it('Should use pagination in channel playlists URL for the canonical tag', async function () {
const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/video-playlists?page=2')
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/video-playlists?page=2" />`)
})
}) })
describe('Indexation tags', function () { describe('Indexation tags', function () {

View File

@ -62,6 +62,20 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:image:url" content="${servers[0].url}/`) expect(text).to.contain(`<meta property="og:image:url" content="${servers[0].url}/`)
} }
async function videosPageTest (path: string) {
const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
const text = res.text
let url = servers[0].url
if (path !== '/') url += path
expect(text).to.match(new RegExp(`<meta property="og:title" content=".* - ${instanceConfig.name}" />`))
expect(text).to.contain(`<meta property="og:description" content="${instanceConfig.shortDescription}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${url}`)
expect(text).to.contain(`<meta property="og:image:url" content="${servers[0].url}/`)
}
async function accountPageTest (path: string) { async function accountPageTest (path: string) {
const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
const text = res.text const text = res.text
@ -108,9 +122,12 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
it('Should have valid Open Graph tags on the common page', async function () { it('Should have valid Open Graph tags on the common page', async function () {
await indexPageTest('/about/peertube') await indexPageTest('/about/peertube')
await indexPageTest('/videos')
await indexPageTest('/homepage') await indexPageTest('/homepage')
await indexPageTest('/') })
it('Should have valid Open Graph tags on the videos page', async function () {
await videosPageTest('/videos/local')
await videosPageTest('/')
}) })
it('Should have valid Open Graph tags on the account page', async function () { it('Should have valid Open Graph tags on the account page', async function () {

View File

@ -11,6 +11,7 @@ import { currentDir, root } from '@peertube/peertube-node-utils'
import { STATIC_MAX_AGE } from '../initializers/constants.js' import { STATIC_MAX_AGE } from '../initializers/constants.js'
import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/html/client-html.js' import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/html/client-html.js'
import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares/index.js' import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares/index.js'
import { VideosOrderType } from '../lib/html/shared/videos-html.js'
const clientsRouter = express.Router() const clientsRouter = express.Router()
@ -29,6 +30,11 @@ clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ],
asyncMiddleware(generateWatchPlaylistHtmlPage) asyncMiddleware(generateWatchPlaylistHtmlPage)
) )
clientsRouter.get([ '/videos/:type(overview|trending|recently-added|local)', '/' ],
clientsRateLimiter,
asyncMiddleware(generateVideosHtmlPage)
)
clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], clientsRouter.use([ '/w/:id', '/videos/watch/:id' ],
clientsRateLimiter, clientsRateLimiter,
asyncMiddleware(generateWatchHtmlPage) asyncMiddleware(generateWatchHtmlPage)
@ -186,6 +192,14 @@ async function generateVideoPlaylistEmbedHtmlPage (req: express.Request, res: ex
return sendHTML(html, res) return sendHTML(html, res)
} }
async function generateVideosHtmlPage (req: express.Request, res: express.Response) {
const { type } = req.params as { type: VideosOrderType }
const html = await ClientHtml.getVideosHTMLPage(type, req, res, req.params.language)
return sendHTML(html, res, true)
}
async function generateWatchHtmlPage (req: express.Request, res: express.Response) { async function generateWatchHtmlPage (req: express.Request, res: express.Response) {
// Thread link is '/w/:videoId;threadId=:threadId' // Thread link is '/w/:videoId;threadId=:threadId'
// So to get the videoId we need to remove the last part // So to get the videoId we need to remove the last part

View File

@ -6,6 +6,8 @@ import { VideoHtml } from './shared/video-html.js'
import { PlaylistHtml } from './shared/playlist-html.js' import { PlaylistHtml } from './shared/playlist-html.js'
import { ActorHtml } from './shared/actor-html.js' import { ActorHtml } from './shared/actor-html.js'
import { PageHtml } from './shared/page-html.js' import { PageHtml } from './shared/page-html.js'
import { VideosHtml, VideosOrderType } from './shared/videos-html.js'
import { CONFIG } from '@server/initializers/config.js'
class ClientHtml { class ClientHtml {
@ -19,6 +21,20 @@ class ClientHtml {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static getVideosHTMLPage (type: VideosOrderType, req: express.Request, res: express.Response, paramLang?: string) {
if (type) {
return VideosHtml.getVideosHTML(type, req, res)
}
const [ , eventualType ] = CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE.split('/videos/') as VideosOrderType[]
if (eventualType) {
return VideosHtml.getVideosHTML(eventualType, req, res)
}
return PageHtml.getDefaultHTML(req, res, paramLang)
}
static getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) { static getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) {
return VideoHtml.getWatchVideoHTML(videoIdArg, req, res) return VideoHtml.getWatchVideoHTML(videoIdArg, req, res)
} }

View File

@ -55,7 +55,25 @@ export class ActorHtml {
let customHTML = TagsHtml.addTitleTag(html, entity.getDisplayName()) let customHTML = TagsHtml.addTitleTag(html, entity.getDisplayName())
customHTML = TagsHtml.addDescriptionTag(customHTML, escapedTruncatedDescription) customHTML = TagsHtml.addDescriptionTag(customHTML, escapedTruncatedDescription)
const url = entity.getClientUrl() const eventualPage = req.path.split('/').pop()
let url
if (entity instanceof AccountModel) {
const page = [ 'video-channels', 'videos' ].includes(eventualPage)
? eventualPage
: undefined
url = entity.getClientUrl(page as 'video-channels' | 'videos')
} else if (entity instanceof VideoChannelModel) {
const page = [ 'video-playlists', 'videos' ].includes(eventualPage)
? eventualPage
: undefined
url = entity.getClientUrl(page as 'video-playlists' | 'videos')
}
if (req.query.page) {
url += `?page=${req.query.page}`
}
const siteName = CONFIG.INSTANCE.NAME const siteName = CONFIG.INSTANCE.NAME
const title = entity.getDisplayName() const title = entity.getDisplayName()

View File

@ -0,0 +1,65 @@
import { escapeHTML } from '@peertube/peertube-core-utils'
import express from 'express'
import { CONFIG } from '../../../initializers/config.js'
import { WEBSERVER } from '../../../initializers/constants.js'
import { PageHtml } from './page-html.js'
import { TagsHtml } from './tags-html.js'
import { ActorImageModel } from '@server/models/actor/actor-image.js'
import { getServerActor } from '@server/models/application/application.js'
import { ActorImageType } from '@peertube/peertube-models'
export type VideosOrderType = 'local' | 'trending' | 'overview' | 'recently-added'
export class VideosHtml {
static async getVideosHTML (type: VideosOrderType, req: express.Request, res: express.Response) {
const html = await PageHtml.getIndexHTML(req, res)
return this.buildVideosHTML({
html,
type,
currentPage: req.query.page
})
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
private static async buildVideosHTML (options: {
html: string
type: VideosOrderType
currentPage: string
}) {
const { html, currentPage, type } = options
const serverActor = await getServerActor()
const avatar = serverActor.getMaxQualityImage(ActorImageType.AVATAR)
const title = type === 'recently-added' ? 'Recently added' : type.slice(0, 1).toUpperCase() + type.slice(1)
let customHTML = TagsHtml.addTitleTag(html, title)
customHTML = TagsHtml.addDescriptionTag(customHTML)
let url = WEBSERVER.URL + '/videos/' + type
if (currentPage) {
url += `?page=${currentPage}`
}
return TagsHtml.addTags(customHTML, {
url,
escapedSiteName: escapeHTML(CONFIG.INSTANCE.NAME),
escapedTitle: `${title} - ${escapeHTML(CONFIG.INSTANCE.NAME)}`,
escapedTruncatedDescription: escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION),
image: avatar
? { url: ActorImageModel.getImageUrl(avatar), width: avatar.width, height: avatar.height }
: undefined,
ogType: 'website',
twitterCard: 'summary_large_image',
forbidIndexation: false
}, {})
}
}

View File

@ -495,8 +495,8 @@ export class AccountModel extends SequelizeModel<AccountModel> {
} }
// Avoid error when running this method on MAccount... | MChannel... // Avoid error when running this method on MAccount... | MChannel...
getClientUrl (this: MAccountHost | MChannelHost) { getClientUrl (this: MAccountHost | MChannelHost, page: 'video-channels' | 'videos' = 'video-channels') {
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels' return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/' + page
} }
isBlocked () { isBlocked () {

View File

@ -859,8 +859,8 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
} }
// Avoid error when running this method on MAccount... | MChannel... // Avoid error when running this method on MAccount... | MChannel...
getClientUrl (this: MAccountHost | MChannelHost) { getClientUrl (this: MAccountHost | MChannelHost, page: 'video-playlists' | 'videos' = 'videos') {
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos' return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/' + page
} }
getDisplayName () { getDisplayName () {