Skip to content

Commit b620bea

Browse files
committed
feat: add canonical tag to pages with pagination
1 parent a85a48f commit b620bea

File tree

9 files changed

+231
-10
lines changed

9 files changed

+231
-10
lines changed

packages/tests/src/client/index-html.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
22

33
import { expect } from 'chai'
4-
import { HttpStatusCode, VideoPlaylistCreateResult } from '@peertube/peertube-models'
4+
import { HttpStatusCode, ServerConfig, VideoPlaylistCreateResult } from '@peertube/peertube-models'
55
import { cleanupTests, makeGetRequest, makeHTMLRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
66
import { checkIndexTags, getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js'
77

@@ -22,6 +22,8 @@ describe('Test index HTML generation', function () {
2222

2323
let instanceDescription: string
2424

25+
const getTitleWithSuffix = (title: string, config: ServerConfig) => `${title} - ${config.instance.name}`
26+
2527
before(async function () {
2628
this.timeout(120000);
2729

@@ -46,7 +48,7 @@ describe('Test index HTML generation', function () {
4648
const config = await servers[0].config.getConfig()
4749
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
4850

49-
checkIndexTags(res.text, 'PeerTube', instanceDescription, '', config)
51+
checkIndexTags(res.text, getTitleWithSuffix('Trending', config), instanceDescription, '', config)
5052
})
5153

5254
it('Should update the customized configuration and have the correct index html tags', async function () {
@@ -70,20 +72,25 @@ describe('Test index HTML generation', function () {
7072
const config = await servers[0].config.getConfig()
7173
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
7274

73-
checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config)
75+
checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config)
7476
})
7577

7678
it('Should have valid index html updated tags (title, description...)', async function () {
7779
const config = await servers[0].config.getConfig()
7880
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
7981

80-
checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config)
82+
checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config)
8183
})
8284
})
8385

8486
describe('Canonical tags', function () {
8587

8688
it('Should use the original video URL for the canonical tag', async function () {
89+
const res = await makeHTMLRequest(servers[0].url, '/videos/trending?page=2')
90+
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/trending?page=2" />`)
91+
})
92+
93+
it('Should use pagination in video URL for the canonical tag', async function () {
8794
for (const basePath of getWatchVideoBasePaths()) {
8895
for (const id of videoIds) {
8996
const res = await makeHTMLRequest(servers[0].url, basePath + id)
@@ -111,6 +118,18 @@ describe('Test index HTML generation', function () {
111118
accountURLtest(await makeHTMLRequest(servers[0].url, '/@root@' + servers[0].host))
112119
})
113120

121+
it('Should use pagination in account video channels URL for the canonical tag', async function () {
122+
const res = await makeHTMLRequest(servers[0].url, '/a/root/video-channels?page=2')
123+
124+
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root/video-channels?page=2" />`)
125+
})
126+
127+
it('Should use pagination in account videos URL for the canonical tag', async function () {
128+
const res = await makeHTMLRequest(servers[0].url, '/a/root/videos?page=2')
129+
130+
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root/videos?page=2" />`)
131+
})
132+
114133
it('Should use the original channel URL for the canonical tag', async function () {
115134
const channelURLtests = res => {
116135
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos" />`)
@@ -120,6 +139,19 @@ describe('Test index HTML generation', function () {
120139
channelURLtests(await makeHTMLRequest(servers[0].url, '/c/root_channel@' + servers[0].host))
121140
channelURLtests(await makeHTMLRequest(servers[0].url, '/@root_channel@' + servers[0].host))
122141
})
142+
143+
it('Should use pagination in channel videos URL for the canonical tag', async function () {
144+
const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/videos?page=2')
145+
146+
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos?page=2" />`)
147+
})
148+
149+
it('Should use pagination in channel playlists URL for the canonical tag', async function () {
150+
const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/video-playlists?page=2')
151+
console.log(res.text)
152+
153+
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/video-playlists?page=2" />`)
154+
})
123155
})
124156

125157
describe('Indexation tags', function () {

server/core/controllers/client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { currentDir, root } from '@peertube/peertube-node-utils'
1111
import { STATIC_MAX_AGE } from '../initializers/constants.js'
1212
import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/html/client-html.js'
1313
import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares/index.js'
14+
import { VideosOrderType } from '../lib/html/shared/videos-html.js'
1415

1516
const clientsRouter = express.Router()
1617

@@ -29,6 +30,11 @@ clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ],
2930
asyncMiddleware(generateWatchPlaylistHtmlPage)
3031
)
3132

33+
clientsRouter.get([ '/videos/:type(overview|trending|recently-added|local)', '/' ],
34+
clientsRateLimiter,
35+
asyncMiddleware(generateVideosHtmlPage)
36+
)
37+
3238
clientsRouter.use([ '/w/:id', '/videos/watch/:id' ],
3339
clientsRateLimiter,
3440
asyncMiddleware(generateWatchHtmlPage)
@@ -186,6 +192,14 @@ async function generateVideoPlaylistEmbedHtmlPage (req: express.Request, res: ex
186192
return sendHTML(html, res)
187193
}
188194

195+
async function generateVideosHtmlPage (req: express.Request, res: express.Response) {
196+
const { type } = req.params as { type: VideosOrderType }
197+
198+
const html = await ClientHtml.getVideosHTMLPage(type, req, res, req.params.language)
199+
200+
return sendHTML(html, res, true)
201+
}
202+
189203
async function generateWatchHtmlPage (req: express.Request, res: express.Response) {
190204
// Thread link is '/w/:videoId;threadId=:threadId'
191205
// So to get the videoId we need to remove the last part

server/core/controllers/index.html

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
7+
<meta name="theme-color" content="#fff" />
8+
<meta property="og:platform" content="PeerTube" />
9+
<!-- Web Manifest file -->
10+
<link rel="manifest" href="/manifest.webmanifest?[manifestContentHash]">
11+
12+
<link rel="icon" type="image/png" href="/client/assets/images/favicon.png?[faviconContentHash]" />
13+
<link rel="apple-touch-icon" href="/client/assets/images/icons/icon-144x144.png" sizes="144x144" />
14+
<link rel="apple-touch-icon" href="/client/assets/images/icons/icon-192x192.png" sizes="192x192" />
15+
16+
<!-- logo background-image -->
17+
<style type="text/css">
18+
.icon-logo {
19+
background-image: url(/client/assets/images/logo.svg?[logoContentHash]);
20+
}
21+
</style>
22+
23+
<!-- base url -->
24+
<base href="/">
25+
26+
<!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
27+
28+
<!-- title tag -->
29+
<!-- description tag -->
30+
<!-- custom css tag -->
31+
<!-- meta tags -->
32+
<!-- server config -->
33+
34+
<!-- /!\ Do not remove it /!\ -->
35+
</head>
36+
37+
<!-- 3. Display the application -->
38+
<body id="custom-css">
39+
40+
<noscript class="alert alert-light">
41+
<h1 class="alert-heading">PeerTube</h1>
42+
<h2 class="mb-3">JavaScript required</h2>
43+
44+
<p>It seems JavaScript is either blocked or disabled in your web browser. We totally get that. However, this page will not work without it.</p>
45+
<p>If you are concerned about the security and privacy (or lack thereof) of JavaScript web applications, you might want to review the source code of the instance you are trying to access, or look for security audits.</p>
46+
47+
<hr>
48+
49+
<h2 class="mb-3">Your options</h2>
50+
51+
<ul>
52+
<li>Allow JavaScript in your browser</li>
53+
<li>Use one of the <a class="link-orange" href="https://docs.joinpeertube.org/use/third-party-application" target="_blank">third-party applications</a> to browse this instance</li>
54+
<li>Review the source code on <a class="link-orange" href="https://github.com/Chocobozzz/PeerTube" target="_blank">GitHub</a> or <a class="link-orange" href="https://framagit.org/framasoft/peertube/PeerTube" target="_blank">Framasoft's GitLab</a>, and ask for modifications from the instance owner.
55+
</ul>
56+
</noscript>
57+
58+
<div id="incompatible-browser" class="alert alert-light" role="alert" style="display: none">
59+
<h1 class="alert-heading">PeerTube</h1>
60+
<h2 class="mb-3">Incompatible browser</h2>
61+
62+
<p>We are sorry but it seems that PeerTube is not compatible with your web browser.</p>
63+
64+
<hr>
65+
<p>Please try with the latest version of <a class="link-orange" href="https://www.mozilla.org" target="_blank">Mozilla Firefox</a>.</p>
66+
<p class="mb-0">If you think this is a mistake, please <a class="link-orange" href="https://github.com/Chocobozzz/PeerTube/issues/new" target="_blank">report it</a>.</p>
67+
</div>
68+
69+
<script type="text/javascript">
70+
function displayIncompatibleBrowser () {
71+
var elem = document.getElementById('incompatible-browser')
72+
if (elem.className.indexOf('browser-ok') === -1) {
73+
elem.style.display = 'block'
74+
}
75+
}
76+
77+
window.onerror = function () {
78+
displayIncompatibleBrowser()
79+
}
80+
81+
if (/MSIE|Trident/.test(window.navigator.userAgent) ) {
82+
displayIncompatibleBrowser()
83+
}
84+
</script>
85+
86+
<my-app>
87+
</my-app>
88+
89+
</body>
90+
</html>

server/core/lib/html/client-html.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { VideoHtml } from './shared/video-html.js'
66
import { PlaylistHtml } from './shared/playlist-html.js'
77
import { ActorHtml } from './shared/actor-html.js'
88
import { PageHtml } from './shared/page-html.js'
9+
import { VideosHtml, VideosOrderType } from './shared/videos-html.js'
10+
import { CONFIG } from '@server/initializers/config.js'
911

1012
class ClientHtml {
1113

@@ -19,6 +21,20 @@ class ClientHtml {
1921

2022
// ---------------------------------------------------------------------------
2123

24+
static getVideosHTMLPage (type: VideosOrderType, req: express.Request, res: express.Response, paramLang?: string) {
25+
if (type) {
26+
return VideosHtml.getVideosHTML(type, req, res)
27+
}
28+
29+
const [ , eventualType ] = CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE.split('/videos/') as VideosOrderType[]
30+
31+
if (eventualType) {
32+
return VideosHtml.getVideosHTML(eventualType, req, res)
33+
}
34+
35+
return PageHtml.getDefaultHTML(req, res, paramLang)
36+
}
37+
2238
static getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) {
2339
return VideoHtml.getWatchVideoHTML(videoIdArg, req, res)
2440
}

server/core/lib/html/shared/actor-html.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,25 @@ export class ActorHtml {
5555
let customHTML = TagsHtml.addTitleTag(html, entity.getDisplayName())
5656
customHTML = TagsHtml.addDescriptionTag(customHTML, escapedTruncatedDescription)
5757

58-
const url = entity.getClientUrl()
58+
const eventualPage = req.path.split('/').pop()
59+
let url
60+
61+
if (entity instanceof AccountModel) {
62+
const page = [ 'video-channels', 'videos' ].includes(eventualPage)
63+
? eventualPage
64+
: undefined
65+
url = entity.getClientUrl(page as 'video-channels' | 'videos')
66+
} else if (entity instanceof VideoChannelModel) {
67+
const page = [ 'video-playlists', 'videos' ].includes(eventualPage)
68+
? eventualPage
69+
: undefined
70+
url = entity.getClientUrl(page as 'video-playlists' | 'videos')
71+
}
72+
73+
if (req.query.page) {
74+
url += `?page=${req.query.page}`
75+
}
76+
5977
const siteName = CONFIG.INSTANCE.NAME
6078
const title = entity.getDisplayName()
6179

server/core/lib/html/shared/video-html.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { PageHtml } from './page-html.js'
1515
import { TagsHtml } from './tags-html.js'
1616

1717
export class VideoHtml {
18-
1918
static async getWatchVideoHTML (videoIdArg: string, req: express.Request, res: express.Response) {
2019
const videoId = toCompleteUUID(videoIdArg)
2120

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { escapeHTML } from '@peertube/peertube-core-utils'
2+
import express from 'express'
3+
import { CONFIG } from '../../../initializers/config.js'
4+
import { WEBSERVER } from '../../../initializers/constants.js'
5+
import { PageHtml } from './page-html.js'
6+
import { TagsHtml } from './tags-html.js'
7+
8+
export type VideosOrderType = 'local' | 'trending' | 'overview' | 'recently-added'
9+
10+
export class VideosHtml {
11+
12+
static async getVideosHTML (type: VideosOrderType, req: express.Request, res: express.Response) {
13+
const html = await PageHtml.getIndexHTML(req, res)
14+
15+
return this.buildVideosHTML({
16+
html,
17+
type,
18+
currentPage: req.query.page
19+
})
20+
}
21+
22+
// ---------------------------------------------------------------------------
23+
// Private
24+
// ---------------------------------------------------------------------------
25+
26+
private static buildVideosHTML (options: {
27+
html: string
28+
type: VideosOrderType
29+
currentPage: string
30+
}) {
31+
const { html, currentPage, type } = options
32+
33+
const title = type === 'recently-added' ? 'Recently added' : type.slice(0, 1).toUpperCase() + type.slice(1)
34+
let customHTML = TagsHtml.addTitleTag(html, title)
35+
customHTML = TagsHtml.addDescriptionTag(customHTML)
36+
37+
let url = WEBSERVER.URL + '/videos/' + type
38+
39+
if (currentPage) {
40+
url += `?page=${currentPage}`
41+
}
42+
43+
return TagsHtml.addTags(customHTML, {
44+
url,
45+
46+
escapedSiteName: escapeHTML(CONFIG.INSTANCE.NAME),
47+
escapedTitle: title,
48+
49+
indexationPolicy: 'always'
50+
}, {})
51+
}
52+
}

server/core/models/account/account.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,8 @@ export class AccountModel extends SequelizeModel<AccountModel> {
476476
}
477477

478478
// Avoid error when running this method on MAccount... | MChannel...
479-
getClientUrl (this: MAccountHost | MChannelHost) {
480-
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels'
479+
getClientUrl (this: MAccountHost | MChannelHost, page: 'video-channels' | 'videos' = 'video-channels') {
480+
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/' + page
481481
}
482482

483483
isBlocked () {

server/core/models/video/video-channel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,8 +841,8 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
841841
}
842842

843843
// Avoid error when running this method on MAccount... | MChannel...
844-
getClientUrl (this: MAccountHost | MChannelHost) {
845-
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos'
844+
getClientUrl (this: MAccountHost | MChannelHost, page: 'video-playlists' | 'videos' = 'videos') {
845+
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/' + page
846846
}
847847

848848
getDisplayName () {

0 commit comments

Comments
 (0)