openvibers/openvibe.bot Commits

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

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

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

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.

* STATUS.json: updated today (the sitemap change)

Changes

ddiff --git a/README.md b/README.md
iindex 4539011..92c28b4 100644
---- a/README.md
++++ b/README.md
@@@ -23,6 +23,8 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato
- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRestream stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).
- Dispatching `platform.job@1` jobs to a paired Node over `/device` (`server/jobs/index.js` over `server/jobs/dispatch.js`; the internal `bot.job.dispatch` API is `POST /api/v1/jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id`) and metering them per wall-clock second (`run_jobs`, `run_usage_outbox`; docs/protocol.md §1.2).
++Sitemap entries use the release commit time for these code-backed pages.
++
## Does not own
- Identity, subjects and tokens (OpenVibe.Network) and the robot's record on Network.
ddiff --git a/server/app.js b/server/app.js
iindex eee3f07..f121e9e 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -134,7 +134,7 @@ function createApp(opts = {}) {
{ const legal = require('openvibe-shared/legal');
app.get(legal.PATHS, (req, res, next) => { res.setHeader('Content-Security-Policy', HOME_CSP); next(); }, legal.handler({ id: 'bot', service: 'bot', host: 'openvibe.bot', name: 'OpenVibe.Bot', profile: 'games' })); }
app.use(viewerMiddleware(userAuth));
-- app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log }));
++ app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));
app.use((req, res) => {
if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex 4611ba7..ce37fae 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -104,7 +104,7 @@ const PANEL_CSP = ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`
const embedCsp = (config) => ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`, `connect-src 'self' ${PANEL_CONNECT_SRC}`,
`frame-ancestors ${frameAncestors(config)}`, "object-src 'none'", "base-uri 'self'"].join('; ');
--function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console } = {}) {
++function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console, releaseLastmod } = {}) {
const r = express.Router();
// The public front page is crawlable; sign-in, the API and every signed-in or per-robot page are not.
r.get('/robots.txt', (req, res) => res.type('text/plain').send(['User-agent: *', 'Allow: /$', 'Disallow: /auth/', 'Disallow: /api/',
@@@ -113,7 +113,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
r.get('/install', (req, res) => res.redirect(302, config.installer.sourceUrl));
// The sitemap lists every public GET page (openvibe-shared/seo); robots.txt already names it.
r.get('/sitemap.xml', (req, res) => {
-- res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}` }))));
++ res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}`, lastmod: releaseLastmod }))));
});
// /llms.txt and /llms-full.txt (llmstxt.org, openvibe-shared/seo): the public pages as a plain-text map for
// language-model crawlers. Only the pages above; the signed-in pages, the API and /internal are not listed.
ddiff --git a/test/legal.test.js b/test/legal.test.js
iindex cebd41a..76b7905 100644
---- a/test/legal.test.js
++++ b/test/legal.test.js
@@@ -3,6 +3,17 @@
// the web app manifest and the browser-facing 404. Bot serves them itself now, and the nginx vhost no longer
// points at a Sites root.
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, check, done } = require('./helpers/app');
@@@ -34,6 +45,7 @@ const { boot, check, done } = require('./helpers/app');
for (const p of ['/', '/install', '/terms', '/privacy', '/dmca']) {
assert.ok(xml.includes(`<loc>http://bot.test${p}</loc>`), `sitemap lists ${p}`);
}
++ assert.ok(assertSitemapLastmods(xml).length >= 5);
});
await check('GET /favicon.ico: the app icon as SVG (browsers ask for it even on a 404 page)', async () => {