openvibers/openvibe.deals Commits

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

OP OpenVibers committed 12 Oct 2026 · e08717e65b · 3 files, +27 −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 8404b03..fd12b4e 100644
---- a/README.md
++++ b/README.md
@@@ -70,6 +70,8 @@ records" obligation; a deviation from the nine-table list, see the final section
| `GET /api/health`, `/api/ready`, `/release.json`, `/metrics` | operations (`/metrics` loopback only; nginx never proxies it) |
| `/auth/*` | Network SSO (the same session layer as Community/Blog) |
++Sitemap entries carry lastmod from each offer or product page's latest recorded change.
++
### API (`/api/v1`, problem+json errors)
Service tokens (audience `openvibe.deals`) are checked for **one capability per route** and act for
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 99b82ae..11a0c30 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -38,7 +38,7 @@ function createDiscoveryRoutes({ config, store, publication, listings }) {
async function productEntries() {
return Promise.all((await listings.products()).map(async (p) => {
const pv = await publication.productView(p);
-- return { loc: abs(publication.productPath(p)), lastmod: pv.freshestObservedAt || undefined, decision: pv.decision };
++ return { loc: abs(publication.productPath(p)), lastmod: Math.max(p.updated_at, ...pv.offers.map((v) => Math.max(v.root.updated_at, v.latest ? v.latest.observed_at : 0))), decision: pv.decision };
}));
}
@@@ -94,7 +94,7 @@ function createDiscoveryRoutes({ config, store, publication, listings }) {
});
router.get('/sitemap.xml', async (_req, res) => {
-- const o = (await offerEntries()).filter((e) => e.decision.indexable).map((e) => e.lastmod);
++ const o = (await offerEntries()).filter((e) => e.decision.indexable).map((e) => e.lastmod).filter(Boolean);
const p = (await productEntries()).filter((e) => e.decision.indexable).map((e) => e.lastmod).filter(Boolean);
const iso = (list) => (list.length ? { lastmod: new Date(Math.max(...list)).toISOString() } : {});
xml(res, seo.sitemapIndex([{ loc: abs('/sitemaps/offers.xml'), ...iso(o) }, { loc: abs('/sitemaps/products.xml'), ...iso(p) }]));
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex 47f7173..9de8fad 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -4,6 +4,17 @@
* sitemaps and Search; discovery artifacts; caching; operations endpoints. No seed deals.
*/
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, check, done, robotsOf, jsonLd, HOUR } = require('./helpers/boot');
(async () => {
@@@ -198,6 +209,18 @@ const { boot, check, done, robotsOf, jsonLd, HOUR } = require('./helpers/boot');
const offers = await t.get('/sitemaps/offers.xml');
const fresh = (await t.ctx.store.db.prepare("SELECT slug FROM deal_offers WHERE title = 'Hardcover novel'").get()).slug;
assert.ok(offers.text.includes(fresh));
++ assertSitemapLastmods(offers.text);
++ const offerRow = await t.ctx.store.db.prepare('SELECT id, updated_at FROM deal_offers WHERE slug = ?').get(fresh);
++ const observation = await t.ctx.store.db.prepare('SELECT MAX(observed_at) AS time FROM deal_price_observations WHERE offer_id = ?').get(offerRow.id);
++ const expected = new Date(Math.max(offerRow.updated_at, observation.time || 0)).toISOString().slice(0, 10);
++ assert.ok(offers.text.includes(`<lastmod>${expected}</lastmod>`));
++ const products = (await t.get('/sitemaps/products.xml')).text;
++ assertSitemapLastmods(products);
++ // An index entry carries its map's newest lastmod; an empty map has no honest date and none is invented.
++ const offersEntry = /<sitemap><loc>[^<]*\/sitemaps\/offers\.xml<\/loc>(<lastmod>[^<]+<\/lastmod>)?<\/sitemap>/.exec(idx.text);
++ assert.ok(offersEntry && offersEntry[1], idx.text);
++ if (!products.includes('<url>')) assert.ok(/<loc>[^<]*\/sitemaps\/products\.xml<\/loc><\/sitemap>/.test(idx.text), 'no lastmod for an empty map');
++ else assertSitemapLastmods(idx.text, 'sitemap');
assert.ok(!offers.text.includes(timed.slug), 'expired');
assert.ok(!offers.text.includes(deal.slug), 'expired');
});