openvibers/openrestream Commits
Sitemaps: every entry carries an honest lastmod (item changes, release time for static pages) (#61)
Changes
@@@ -159,7 +159,7 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {app.use('/auth', createSsoRoutes({ config, auth, fetchImpl }));// Crawl and machine-readability artifacts (robots.txt, sitemap.xml, llms.txt): openvibe-shared/seo.-- app.use('/', createDiscoveryRoutes({ config }));++ app.use('/', createDiscoveryRoutes({ config, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));app.use('/', createUiRouter({ rt, auth, limits: actorLimits, liveLink }));app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));@@@ -19,7 +19,7 @@ const seo = require('openvibe-shared/seo');const PUBLIC_PATHS = ['/', '/updates'];const DISALLOW = ['/streams', '/sessions', '/destinations', '/auth/', '/api/', '/play/'];--function createDiscoveryRoutes({ config }) {++function createDiscoveryRoutes({ config, releaseLastmod }) {const router = express.Router();const abs = (p) => seo.absolute(p, config.baseUrl);@@@ -27,7 +27,7 @@ function createDiscoveryRoutes({ config }) {seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW })));router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders()).send(-- seo.sitemapXml(PUBLIC_PATHS.map((loc) => ({ loc: abs(loc) })))));++ seo.sitemapXml(PUBLIC_PATHS.map((loc) => ({ loc: abs(loc), lastmod: releaseLastmod })))));router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders()).send(seo.llmsTxt({@@@ -3,6 +3,17 @@// names the sitemap, sitemap.xml lists the public pages including /, and llms.txt describes what// OpenRestream does today. All three come from openvibe-shared/seo, the shared toolkit.const assert = require('assert');++++function assertSitemapLastmods(xml, tag = 'url') {++ const entries = [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'g'))];++ for (const [, body] of entries) {++ const match = /<lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod>/.exec(body);++ assert.ok(match, body);++ assert.strictEqual(new Date(match[1]).toISOString().slice(0, 10), match[1]);++ }++ return entries;++}++const seo = require('openvibe-shared/seo');const social = require('openvibe-shared/social');const { bootApi, request, suite, userToken, OWNER } = require('./helpers');@@@ -31,6 +42,7 @@ t('sitemap.xml is XML and lists the public pages, including /', async () => {assert.match(r.headers.get('content-type'), /xml/);assert.ok(r.text.includes(`<loc>${seo.absolute('/', api.config.baseUrl)}</loc>`), 'lists /');assert.ok(r.text.includes(`<loc>${seo.absolute('/updates', api.config.baseUrl)}</loc>`), 'lists /updates');++ assert.strictEqual(assertSitemapLastmods(r.text).length, 2);} finally {await api.close();}
@@ -159,7 +159,7 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
@@ -159,7 +159,7 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
app.use('/auth', createSsoRoutes({ config, auth, fetchImpl }));
app.use('/auth', createSsoRoutes({ config, auth, fetchImpl }));
// Crawl and machine-readability artifacts (robots.txt, sitemap.xml, llms.txt): openvibe-shared/seo.
// Crawl and machine-readability artifacts (robots.txt, sitemap.xml, llms.txt): openvibe-shared/seo.
- app.use('/', createDiscoveryRoutes({ config }));
+ app.use('/', createDiscoveryRoutes({ config, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));
app.use('/', createUiRouter({ rt, auth, limits: actorLimits, liveLink }));
app.use('/', createUiRouter({ rt, auth, limits: actorLimits, liveLink }));
app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
@@ -19,7 +19,7 @@ const seo = require('openvibe-shared/seo');
@@ -19,7 +19,7 @@ const seo = require('openvibe-shared/seo');
const PUBLIC_PATHS = ['/', '/updates'];
const PUBLIC_PATHS = ['/', '/updates'];
const DISALLOW = ['/streams', '/sessions', '/destinations', '/auth/', '/api/', '/play/'];
const DISALLOW = ['/streams', '/sessions', '/destinations', '/auth/', '/api/', '/play/'];
-function createDiscoveryRoutes({ config }) {
+function createDiscoveryRoutes({ config, releaseLastmod }) {
const router = express.Router();
const router = express.Router();
const abs = (p) => seo.absolute(p, config.baseUrl);
const abs = (p) => seo.absolute(p, config.baseUrl);
@@ -27,7 +27,7 @@ function createDiscoveryRoutes({ config }) {
@@ -27,7 +27,7 @@ function createDiscoveryRoutes({ config }) {
seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW })));
seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW })));
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders()).send(
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders()).send(
- seo.sitemapXml(PUBLIC_PATHS.map((loc) => ({ loc: abs(loc) })))));
+ seo.sitemapXml(PUBLIC_PATHS.map((loc) => ({ loc: abs(loc), lastmod: releaseLastmod })))));
router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders()).send(
router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders()).send(
seo.llmsTxt({
seo.llmsTxt({
@@ -3,6 +3,17 @@
@@ -3,6 +3,17 @@
// names the sitemap, sitemap.xml lists the public pages including /, and llms.txt describes what
// names the sitemap, sitemap.xml lists the public pages including /, and llms.txt describes what
// OpenRestream does today. All three come from openvibe-shared/seo, the shared toolkit.
// OpenRestream does today. All three come from openvibe-shared/seo, the shared toolkit.
const assert = require('assert');
const assert = require('assert');
+
+function assertSitemapLastmods(xml, tag = 'url') {
+ const entries = [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'g'))];
+ for (const [, body] of entries) {
+ const match = /<lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod>/.exec(body);
+ assert.ok(match, body);
+ assert.strictEqual(new Date(match[1]).toISOString().slice(0, 10), match[1]);
+ }
+ return entries;
+}
+
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
const social = require('openvibe-shared/social');
const social = require('openvibe-shared/social');
const { bootApi, request, suite, userToken, OWNER } = require('./helpers');
const { bootApi, request, suite, userToken, OWNER } = require('./helpers');
@@ -31,6 +42,7 @@ t('sitemap.xml is XML and lists the public pages, including /', async () => {
@@ -31,6 +42,7 @@ t('sitemap.xml is XML and lists the public pages, including /', async () => {
assert.match(r.headers.get('content-type'), /xml/);
assert.match(r.headers.get('content-type'), /xml/);
assert.ok(r.text.includes(`<loc>${seo.absolute('/', api.config.baseUrl)}</loc>`), 'lists /');
assert.ok(r.text.includes(`<loc>${seo.absolute('/', api.config.baseUrl)}</loc>`), 'lists /');
assert.ok(r.text.includes(`<loc>${seo.absolute('/updates', api.config.baseUrl)}</loc>`), 'lists /updates');
assert.ok(r.text.includes(`<loc>${seo.absolute('/updates', api.config.baseUrl)}</loc>`), 'lists /updates');
+ assert.strictEqual(assertSitemapLastmods(r.text).length, 2);
} finally {
} finally {
await api.close();
await api.close();
}
}