openvibers/openvibe.events Commits
Sitemaps: every entry carries an honest lastmod (item changes, release time for static pages) (#67)
Changes
@@@ -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.++## PurposeThe 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.@@@ -97,7 +97,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLoapp.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@@@ -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({@@@ -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 () => {
@@ -7,6 +7,8 @@
@@ -7,6 +7,8 @@
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §6 and §6.3.
**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).
**License:** AGPL-3.0 (same as every OpenVibe service).
+The home page sitemap entry uses the release commit time as lastmod.
+
## Purpose
## 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.
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.
@@ -97,7 +97,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
@@ -97,7 +97,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
app.use(resourcesRouter({ store, auth }));
app.use(resourcesRouter({ store, auth }));
// Discovery for the product domain (openvibe.events): robots.txt, sitemap.xml, llms.txt.
// 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
// 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
// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
@@ -36,7 +36,7 @@ function details({ siteUrl, apiUrl }) {
@@ -36,7 +36,7 @@ function details({ siteUrl, apiUrl }) {
].join('\n');
].join('\n');
}
}
-function createDiscoveryRoutes({ config }) {
+function createDiscoveryRoutes({ config, releaseLastmod }) {
const router = express.Router();
const router = express.Router();
const abs = (p) => seo.absolute(p, config.siteUrl);
const abs = (p) => seo.absolute(p, config.siteUrl);
@@ -44,7 +44,7 @@ function createDiscoveryRoutes({ config }) {
@@ -44,7 +44,7 @@ function createDiscoveryRoutes({ config }) {
.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
.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())
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())
router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())
.send(seo.llmsTxt({
.send(seo.llmsTxt({
@@ -4,6 +4,17 @@
@@ -4,6 +4,17 @@
// Frame at /shared, answers robots.txt, sitemap.xml and llms.txt, and names its own origin everywhere.
// 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.
// A parse-only check of the nginx vhost too.
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 fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const nodeHttp = require('http');
const nodeHttp = require('http');
@@ -130,6 +141,7 @@ t('sitemap.xml lists the product home only', async () => {
@@ -130,6 +141,7 @@ t('sitemap.xml lists the product home only', async () => {
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers['content-type'], /xml/);
assert.match(r.headers['content-type'], /xml/);
assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);
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 () => {
t('llms.txt is a plain-text map that starts with the site name', async () => {