openvibers/openvibe.coupons Commits

llms.txt: the merchant lookup is a working URL, not a bare path (SEO-34) (#136)

OP OpenVibers committed 15 days ago · 52e3af2700 · 3 files, +14 −2

GET /api/v1/merchants/resolve requires ?host=; called without it the route answers 400 by design, so the URL the file offered was one a language model cannot use. canonicalUrl keeps a query parameter only when it is named, so the link now passes the host through a worked example and the note states the shape. The discovery test sweeps every URL in the file for an answer below 400 and pins the required parameter.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 807d52d..402739f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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.
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 13d03a4..6d0f128 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -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' },
] },
],
}));
ddiff --git a/test/pages-discovery.test.js b/test/pages-discovery.test.js
iindex 40543a1..47feaee 100644
---- a/test/pages-discovery.test.js
++++ b/test/pages-discovery.test.js
@@@ -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}`));