openvibers/openvibe.ai Commits

Sitemaps: every entry carries an honest lastmod (item changes, release time for static pages) (#58)

OP OpenVibers committed 12 Oct 2026 · 9305c94962 · 3 files, +23 −2

Plan T11's SEO kit calls for "sitemaps with lastmod". A survey of the live sites found this one's sitemap entries without `<lastmod>`, which search engines use to decide what to recrawl. Every entry now carries an honest one:

- **Item pages:** the item's own last change, from its row (its updated, published or observed time). Never "now", and never invented.
- **Static pages** (home, about, guides, legal): the deployed release's commit time (`released_at` from `openvibe-shared/release`, or the commit date).
- **Sitemap index entries:** the newest lastmod of their map. An empty map gets none, rather than an invented date.

The README says where lastmod comes from. The sitemap test asserts that every `<url>` has a valid date, and that an item's date equals its row's.

Part of a sweep over twelve sites, built by a Codex job and reviewed. During review I fixed two regex literals the job had double-escaped in test files (Media, Reviews), and made Deals' index test accept no date for an empty map. Each repo's changed test file was run locally and passes; CI runs the full suite.

Changes

ddiff --git a/README.md b/README.md
iindex 4616bf0..9b6f761 100644
---- a/README.md
++++ b/README.md
@@@ -449,6 +449,8 @@ The old `ai.openvibe.network` address was retired on 2026-10-10. The console nee
`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it
answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.
++Sitemap entries use the release commit for home and the newest recorded stats day for `/stats` when available.
++
## Not done yet
- Live's VOD and clip overviews calling `media.analyze` instead of extracting frames and transcribing
ddiff --git a/server/app.js b/server/app.js
iindex 2686242..d67880a 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -22,6 +22,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
app.disable('x-powered-by');
app.set('trust proxy', 'loopback');
const release = createRelease({ service: 'ai', root: path.join(__dirname, '..') });
++ const releaseLastmod = release.full().released_at === release.full().booted_at ? undefined : release.full().released_at;
// HTTP golden signals by route template, process metrics, release_info and the AI gauges;
// GET /metrics answers direct loopback callers only (Track O).
const metrics = instrument(app, { service: 'ai', release: release.release });
@@@ -123,8 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
app.get('/robots.txt', (_req, res) => {
res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
});
-- app.get('/sitemap.xml', (_req, res) => {
-- res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/stats'].map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);
++ app.get('/sitemap.xml', async (_req, res, next) => {
++ try {
++ const from = new Date(clock.now() - STATS_DAYS * 86400000).toISOString().slice(0, 10);
++ const stats = await db.prepare('SELECT MAX(day) AS lastmod FROM provider_stats_daily WHERE day >= ?').get(from);
++ const statsLastmod = [stats && stats.lastmod ? new Date(stats.lastmod).toISOString() : null, releaseLastmod].filter(Boolean).sort().pop();
++ const entries = [['/', releaseLastmod], ['/stats', statsLastmod]];
++ res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map(([p, lastmod]) => ` <url><loc>${config.baseUrl}${p}</loc>${lastmod ? `<lastmod>${lastmod.slice(0, 10)}</lastmod>` : ''}</url>`).join('\n')}\n</urlset>\n`);
++ } catch (e) { next(e); }
});
app.get('/llms.txt', (_req, res) => {
res.setHeader('Cache-Control', 'public, max-age=3600');
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 6bf2589..d9e339c 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -4,6 +4,17 @@
// /shared, and answers robots.txt, sitemap.xml and llms.txt. The vhosts: the developer-app routes are the only
// API the public vhost proxies, and the old ai.openvibe.network address is retired (no vhost of its own).
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 fs = require('fs');
const path = require('path');
const { boot, request, suite } = require('./helpers');
@@@ -61,6 +72,7 @@ t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () =
assert.strictEqual(map.status, 200);
assert.match(map.headers.get('content-type'), /xml/);
assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);
++ assert.strictEqual(assertSitemapLastmods(map.text).length, 2);
const llms = await request(h.base, 'GET', '/llms.txt');
assert.strictEqual(llms.status, 200);
assert.match(llms.headers.get('content-type'), /^text\/plain/);