openvibers/openvibe.coupons Commits
llms.txt: the merchant lookup is a working URL, not a bare path (SEO-34) (#136)
Changes
@@@ -2,6 +2,7 @@## Unreleased++- `llms.txt` lists the merchant lookup correctly (SEO-34): `GET /api/v1/merchants/resolve` needs `?host=`, and `canonicalUrl` drops a query string unless it is named, so the file advertised a URL that answers 400 to a plain GET. The link is now a worked example (`?host=amazon.com`) and `test/pages-discovery.test.js` sweeps every URL the file lists for a non-error answer and pins the parameter.- The front page is curated (`server/render/home.js`): a shop search and the site's numbers beside the hero, then codes reported working, ending soon and new as tickets that show the code itself (the list showed only deal headlines), the shops, the Companion, what every code tells you and six questions (FAQPage, ItemList and WebSite JSON-LD); the page is 1080 px wide. DealNews' feed terms are kept (`server/domain/source-policy.js`): its codes say "via DealNews" on every page and never reach a browser extension (extension origins and install tokens). Budgets: html 33.7 KB, css 21.8 KB.- `openvibe-contracts` is pinned to v0.97.0 (and `openvibe-shared` to v2.9.0): all six Coupons capabilities are released there, so the local proposed-id fallback in `server/auth/capabilities.js` is gone and `checkCapability` is the library's check. STATUS.json and the README pins now match package.json; `test/capabilities.test.js` locks the released ids, the delegated check and the Coupons-local install scopes.- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; robots and the canonical still come from the indexability gate's decision. The shell adds `web-runtime.js` to every page; the home page's JS budget still holds (5 files, 239.1 KB, 56.3 KB brotli), so no budget was raised.@@@ -24,7 +24,7 @@ const pages = require('../render/pages');function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {const router = express.Router();-- const abs = (p) => seo.canonicalUrl(config.baseUrl, p);++ const abs = (p, opts) => seo.canonicalUrl(config.baseUrl, p, opts);const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);async function activeEntries() {@@@ -69,7 +69,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication{ title: 'Feeds', links: [{ title: 'RSS', url: abs('/feed.xml') }, { title: 'Atom', url: abs('/atom.xml') }, { title: 'JSON Feed', url: abs('/feed.json') }] },{ title: 'Data', links: [{ title: 'Shop JSON', url: abs('/'), note: 'append .json to a shop URL (/m/<slug>.json)' },-- { title: 'Merchant lookup API', url: abs('/api/v1/merchants/resolve?host=example.com'), note: 'GET /api/v1/merchants/resolve?host= and GET /api/v1/merchants/<id>/coupons; anonymous, rate-limited' },++ { title: 'Merchant lookup API', url: abs('/api/v1/merchants/resolve?host=amazon.com', { query: ['host'] }), note: 'GET /api/v1/merchants/resolve?host=<shop host> (a worked example above) and GET /api/v1/merchants/<id>/coupons; anonymous, rate-limited' },] },],}));@@@ -104,6 +104,17 @@ const { boot, check, done } = require('./helpers/boot');const llms = await t.get('/llms.txt');assert.match(llms.text, /reported_working/);assert.match(llms.text, /\/m\/<slug>\.json/);++ // Every URL the file lists has to answer when it is fetched as a link: a language model that++ // follows one and gets a 400 has learned nothing. The merchant lookup is a GET with one++ // required parameter, so its link carries the parameter; bare, it answers 400 by design.++ for (const url of [...llms.text.matchAll(/\((https?:\/\/[^)]+)\)/g)].map((m) => m[1])) {++ if (!url.startsWith('https://openvibe.coupons/') || url.includes('/api/')) continue;++ const u = new URL(url);++ assert.ok((await t.get(u.pathname + u.search)).status < 400, url + ' does not answer');++ }++ const lookup = /https:\/\/openvibe\.coupons\/api\/v1\/merchants\/resolve\?host=([a-z0-9.-]+)/.exec(llms.text);++ assert.ok(lookup, 'the merchant lookup link does not carry its required host parameter');++ assert.strictEqual((await t.get('/api/v1/merchants/resolve?host=greengrocer.co.uk')).status, 200, 'the merchant lookup answers a plain GET with a host');assert.match((await t.get('/sitemap.xml')).text, /sitemaps\/coupons\.xml[\s\S]*sitemaps\/merchants\.xml|sitemaps\/merchants\.xml[\s\S]*sitemaps\/coupons\.xml/);assert.match((await t.get('/sitemaps/coupons.xml')).text, new RegExp(`https://openvibe\\.coupons/c/${codeId}`));assert.match((await t.get('/sitemaps/merchants.xml')).text, new RegExp(`https://openvibe\\.coupons/m/${shop.slug}`));
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
## Unreleased
## Unreleased
+- `llms.txt` lists the merchant lookup correctly (SEO-34): `GET /api/v1/merchants/resolve` needs `?host=`, and `canonicalUrl` drops a query string unless it is named, so the file advertised a URL that answers 400 to a plain GET. The link is now a worked example (`?host=amazon.com`) and `test/pages-discovery.test.js` sweeps every URL the file lists for a non-error answer and pins the parameter.
- The front page is curated (`server/render/home.js`): a shop search and the site's numbers beside the hero, then codes reported working, ending soon and new as tickets that show the code itself (the list showed only deal headlines), the shops, the Companion, what every code tells you and six questions (FAQPage, ItemList and WebSite JSON-LD); the page is 1080 px wide. DealNews' feed terms are kept (`server/domain/source-policy.js`): its codes say "via DealNews" on every page and never reach a browser extension (extension origins and install tokens). Budgets: html 33.7 KB, css 21.8 KB.
- The front page is curated (`server/render/home.js`): a shop search and the site's numbers beside the hero, then codes reported working, ending soon and new as tickets that show the code itself (the list showed only deal headlines), the shops, the Companion, what every code tells you and six questions (FAQPage, ItemList and WebSite JSON-LD); the page is 1080 px wide. DealNews' feed terms are kept (`server/domain/source-policy.js`): its codes say "via DealNews" on every page and never reach a browser extension (extension origins and install tokens). Budgets: html 33.7 KB, css 21.8 KB.
- `openvibe-contracts` is pinned to v0.97.0 (and `openvibe-shared` to v2.9.0): all six Coupons capabilities are released there, so the local proposed-id fallback in `server/auth/capabilities.js` is gone and `checkCapability` is the library's check. STATUS.json and the README pins now match package.json; `test/capabilities.test.js` locks the released ids, the delegated check and the Coupons-local install scopes.
- `openvibe-contracts` is pinned to v0.97.0 (and `openvibe-shared` to v2.9.0): all six Coupons capabilities are released there, so the local proposed-id fallback in `server/auth/capabilities.js` is gone and `checkCapability` is the library's check. STATUS.json and the README pins now match package.json; `test/capabilities.test.js` locks the released ids, the delegated check and the Coupons-local install scopes.
- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; robots and the canonical still come from the indexability gate's decision. The shell adds `web-runtime.js` to every page; the home page's JS budget still holds (5 files, 239.1 KB, 56.3 KB brotli), so no budget was raised.
- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; robots and the canonical still come from the indexability gate's decision. The shell adds `web-runtime.js` to every page; the home page's JS budget still holds (5 files, 239.1 KB, 56.3 KB brotli), so no budget was raised.
@@ -24,7 +24,7 @@ const pages = require('../render/pages');
@@ -24,7 +24,7 @@ const pages = require('../render/pages');
function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {
function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {
const router = express.Router();
const router = express.Router();
- const abs = (p) => seo.canonicalUrl(config.baseUrl, p);
+ const abs = (p, opts) => seo.canonicalUrl(config.baseUrl, p, opts);
const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);
const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);
async function activeEntries() {
async function activeEntries() {
@@ -69,7 +69,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
@@ -69,7 +69,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
{ title: 'Feeds', links: [{ title: 'RSS', url: abs('/feed.xml') }, { title: 'Atom', url: abs('/atom.xml') }, { title: 'JSON Feed', url: abs('/feed.json') }] },
{ title: 'Feeds', links: [{ title: 'RSS', url: abs('/feed.xml') }, { title: 'Atom', url: abs('/atom.xml') }, { title: 'JSON Feed', url: abs('/feed.json') }] },
{ title: 'Data', links: [
{ title: 'Data', links: [
{ title: 'Shop JSON', url: abs('/'), note: 'append .json to a shop URL (/m/<slug>.json)' },
{ title: 'Shop JSON', url: abs('/'), note: 'append .json to a shop URL (/m/<slug>.json)' },
- { title: 'Merchant lookup API', url: abs('/api/v1/merchants/resolve?host=example.com'), note: 'GET /api/v1/merchants/resolve?host= and GET /api/v1/merchants/<id>/coupons; anonymous, rate-limited' },
+ { title: 'Merchant lookup API', url: abs('/api/v1/merchants/resolve?host=amazon.com', { query: ['host'] }), note: 'GET /api/v1/merchants/resolve?host=<shop host> (a worked example above) and GET /api/v1/merchants/<id>/coupons; anonymous, rate-limited' },
] },
] },
],
],
}));
}));
@@ -104,6 +104,17 @@ const { boot, check, done } = require('./helpers/boot');
@@ -104,6 +104,17 @@ const { boot, check, done } = require('./helpers/boot');
const llms = await t.get('/llms.txt');
const llms = await t.get('/llms.txt');
assert.match(llms.text, /reported_working/);
assert.match(llms.text, /reported_working/);
assert.match(llms.text, /\/m\/<slug>\.json/);
assert.match(llms.text, /\/m\/<slug>\.json/);
+ // Every URL the file lists has to answer when it is fetched as a link: a language model that
+ // follows one and gets a 400 has learned nothing. The merchant lookup is a GET with one
+ // required parameter, so its link carries the parameter; bare, it answers 400 by design.
+ for (const url of [...llms.text.matchAll(/\((https?:\/\/[^)]+)\)/g)].map((m) => m[1])) {
+ if (!url.startsWith('https://openvibe.coupons/') || url.includes('/api/')) continue;
+ const u = new URL(url);
+ assert.ok((await t.get(u.pathname + u.search)).status < 400, url + ' does not answer');
+ }
+ const lookup = /https:\/\/openvibe\.coupons\/api\/v1\/merchants\/resolve\?host=([a-z0-9.-]+)/.exec(llms.text);
+ assert.ok(lookup, 'the merchant lookup link does not carry its required host parameter');
+ assert.strictEqual((await t.get('/api/v1/merchants/resolve?host=greengrocer.co.uk')).status, 200, 'the merchant lookup answers a plain GET with a host');
assert.match((await t.get('/sitemap.xml')).text, /sitemaps\/coupons\.xml[\s\S]*sitemaps\/merchants\.xml|sitemaps\/merchants\.xml[\s\S]*sitemaps\/coupons\.xml/);
assert.match((await t.get('/sitemap.xml')).text, /sitemaps\/coupons\.xml[\s\S]*sitemaps\/merchants\.xml|sitemaps\/merchants\.xml[\s\S]*sitemaps\/coupons\.xml/);
assert.match((await t.get('/sitemaps/coupons.xml')).text, new RegExp(`https://openvibe\\.coupons/c/${codeId}`));
assert.match((await t.get('/sitemaps/coupons.xml')).text, new RegExp(`https://openvibe\\.coupons/c/${codeId}`));
assert.match((await t.get('/sitemaps/merchants.xml')).text, new RegExp(`https://openvibe\\.coupons/m/${shop.slug}`));
assert.match((await t.get('/sitemaps/merchants.xml')).text, new RegExp(`https://openvibe\\.coupons/m/${shop.slug}`));