openvibers/openvibe.events Commits

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

OP OpenVibers committed 12 Oct 2026 · cc39ec4da7 · 4 files, +17 −3

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 a72c5eb..5413631 100644
---- a/README.md
++++ b/README.md
@@@ -7,6 +7,8 @@
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §6 and §6.3.
**License:** AGPL-3.0 (same as every OpenVibe service).
++The home page sitemap entry uses the release commit time as lastmod.
++
## Purpose
The durable event backbone the network does not have yet: every authoritative service writes domain mutations and outbox events in one transaction, a relay publishes them here, consumers use inbox/idempotency receipts, and browsers get authorized, resumable projections through the Realtime delivery plane.
ddiff --git a/server/app.js b/server/app.js
iindex 2550e76..866db28 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -97,7 +97,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
app.use(resourcesRouter({ store, auth }));
// Discovery for the product domain (openvibe.events): robots.txt, sitemap.xml, llms.txt.
-- app.use(createDiscoveryRoutes({ config }));
++ app.use(createDiscoveryRoutes({ config, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));
// GET / on the product domain: the home page for a browser (server/home.js, openvibe.events), the
// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 50fd942..7fbab6d 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -36,7 +36,7 @@ function details({ siteUrl, apiUrl }) {
].join('\n');
}
--function createDiscoveryRoutes({ config }) {
++function createDiscoveryRoutes({ config, releaseLastmod }) {
const router = express.Router();
const abs = (p) => seo.absolute(p, config.siteUrl);
@@@ -44,7 +44,7 @@ function createDiscoveryRoutes({ config }) {
.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())
-- .send(seo.sitemapXml([{ loc: abs('/') }])));
++ .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }])));
router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())
.send(seo.llmsTxt({
ddiff --git a/test/home.test.js b/test/home.test.js
iindex d24ce4e..4df6338 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -4,6 +4,17 @@
// Frame at /shared, answers robots.txt, sitemap.xml and llms.txt, and names its own origin everywhere.
// A parse-only check of the nginx vhost too.
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 nodeHttp = require('http');
@@@ -130,6 +141,7 @@ t('sitemap.xml lists the product home only', async () => {
assert.strictEqual(r.status, 200);
assert.match(r.headers['content-type'], /xml/);
assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);
++ assert.strictEqual(assertSitemapLastmods(r.body).length, 1);
});
t('llms.txt is a plain-text map that starts with the site name', async () => {