openvibers/openvibe.reviews Commits

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

OP OpenVibers committed 12 Oct 2026 · e7c7414456 · 4 files, +34 −7

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 0f604e6..4703ea7 100644
---- a/README.md
++++ b/README.md
@@@ -138,6 +138,8 @@ secret, Community and editors reported), `/release.json`, `/metrics` (outbox and
| `POST /internal/events` | OpenVibe.Events deliveries (`sources.item.*`), HMAC-signed; not proxied publicly |
| `/auth/*` | Network SSO (client `reviews`) |
++Sitemap entries carry lastmod from entity updates, the newest entity on the home page, or the release commit for `/about`.
++
## API `/api/v1`
People use their Network JWT; services use a client-credentials token for audience
ddiff --git a/server/app.js b/server/app.js
iindex b388841..9e1cf35 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -156,7 +156,7 @@ function createApp({ config, svc, viewers, platform, sources, sync, keys, db, lo
res.setHeader('Cache-Control', cache.assetHeaders(rel, { hashed: !!v && v === require('./render/layout').assetVersion(rel) }));
},
}));
-- app.use(createMachine({ svc, config }));
++ app.use(createMachine({ svc, config, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));
app.post(['/e/*', '/editor/*'], limiter(10 * 60000, 120));
app.post('/e/:slug/correct', limiter(60 * 60000, 20));
ddiff --git a/server/http/machine.js b/server/http/machine.js
iindex 785decc..ae13a19 100644
---- a/server/http/machine.js
++++ b/server/http/machine.js
@@@ -19,7 +19,7 @@ const { SITE_SUMMARY } = require('../render/layout');
const PER_SITEMAP = 45000;
const ALL_SUMMARIES = 50000;
--function createMachine({ svc, config }) {
++function createMachine({ svc, config, releaseLastmod }) {
const router = express.Router();
const origin = config.baseUrl;
const setCache = (res, s = 300) => res.set('Cache-Control', cache.htmlHeaders({ maxAge: s }));
@@@ -36,14 +36,23 @@ function createMachine({ svc, config }) {
}
router.get('/sitemap.xml', async (_req, res) => {
-- const pages = seo.sitemap(await entityEntries(), { maxUrls: PER_SITEMAP });
-- const maps = [{ loc: `${origin}/sitemaps/pages.xml` }];
-- pages.files.forEach((_f, i) => maps.push({ loc: `${origin}/sitemaps/entities-${i + 1}.xml` }));
++ const entries = (await entityEntries()).filter((e) => e.decision.indexable);
++ const pages = seo.sitemap(entries, { maxUrls: PER_SITEMAP });
++ const updated = entries.map((e) => e.lastmod).filter(Boolean);
++ const homeLastmod = updated.length ? new Date(Math.max(...updated)).toISOString() : releaseLastmod;
++ const pagesLastmod = [homeLastmod, releaseLastmod].filter(Boolean).sort().pop();
++ const maps = [{ loc: `${origin}/sitemaps/pages.xml`, lastmod: pagesLastmod }];
++ pages.files.forEach((_f, i) => {
++ const times = entries.slice(i * PER_SITEMAP, (i + 1) * PER_SITEMAP).map((e) => e.lastmod).filter(Boolean);
++ maps.push({ loc: `${origin}/sitemaps/entities-${i + 1}.xml`, lastmod: times.length ? new Date(Math.max(...times)).toISOString() : undefined });
++ });
setCache(res).type('application/xml').send(seo.sitemapIndex(maps));
});
-- router.get('/sitemaps/pages.xml', (_req, res) => {
++ router.get('/sitemaps/pages.xml', async (_req, res) => {
++ const updated = (await entityEntries()).filter((e) => e.decision.indexable && e.lastmod).map((e) => e.lastmod);
++ const homeLastmod = updated.length ? new Date(Math.max(...updated)).toISOString() : releaseLastmod;
const policy = { minWords: 0, requireSources: false };
-- const entries = ['/', '/about'].map((p) => ({ loc: seo.canonicalUrl(origin, p), decision: seo.evaluate({ state: 'published', visibility: 'public', canonicalUrl: seo.canonicalUrl(origin, p), wordCount: 0 }, { policy, now: Date.now() }) }));
++ const entries = ['/', '/about'].map((p) => ({ loc: seo.canonicalUrl(origin, p), lastmod: p === '/' ? homeLastmod : releaseLastmod, decision: seo.evaluate({ state: 'published', visibility: 'public', canonicalUrl: seo.canonicalUrl(origin, p), wordCount: 0 }, { policy, now: Date.now() }) }));
setCache(res).type('application/xml').send(seo.sitemap(entries).files[0]);
});
router.get('/sitemaps/entities-:n.xml', async (req, res) => {
ddiff --git a/test/nojs.test.js b/test/nojs.test.js
iindex dd1e2fa..94964c5 100644
---- a/test/nojs.test.js
++++ b/test/nojs.test.js
@@@ -5,6 +5,17 @@
* JSON-LD is absent without signals.
*/
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 {
boot, req, suite, steamItem, productItem, importItem, createEntity, deliver, sourcesEvent, cookieFor, editorToken, readerToken, fakeCommunity,
} = require('./helpers');
@@@ -87,8 +98,13 @@ t('every public page reads without JavaScript; sitemaps, feeds and robots follow
assert.ok(jsonLd(page.text).some((x) => x['@type'] === 'Review'));
sm = await req(h, 'GET', '/sitemaps/entities-1.xml');
assert.ok(sm.text.includes(`http://reviews.test/e/${e.slug}`));
++ assertSitemapLastmods(sm.text);
++ const entityRow = await h.db.prepare('SELECT updated_at FROM review_entities WHERE id = ?').get(e.id);
++ assert.ok(sm.text.includes(`<lastmod>${new Date(entityRow.updated_at).toISOString().slice(0, 10)}</lastmod>`));
const idx = await req(h, 'GET', '/sitemap.xml');
assert.match(idx.text, /sitemaps\/entities-1\.xml/);
++ assertSitemapLastmods(idx.text, 'sitemap');
++ assertSitemapLastmods((await req(h, 'GET', '/sitemaps/pages.xml')).text);
const atom = await req(h, 'GET', '/feed.atom');
assert.strictEqual(atom.status, 200);
assert.match(atom.text, /Portal 2: summary/);