openvibers/openvibe.reviews Commits
Adopt openvibe-publishing 1.3.0 and add /llms-full.txt with the site…
Changes
@@@ -9,7 +9,7 @@"domain": "openvibe.reviews","contracts": "openvibe-contracts v0.76.0","packages": [-- "openvibe-publishing v1.2.0",++ "openvibe-publishing v1.3.0","openvibe-shared v2.6.0","openvibe-sdk v0.26.0"],@@@ -15,7 +15,7 @@"express-rate-limit": "^7.5.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0","pg": "^8.23.0"@@@ -1149,9 +1149,9 @@}},"node_modules/openvibe-publishing": {-- "version": "1.2.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",-- "integrity": "sha512-n/20yxXo5jRs0wi7KsR+R9jDkGsoBak3gqElHV6XyU+i1t3XCzaoz+sDb10G2cbc4uEaNB/F09VtQKrQzLp6YQ==",++ "version": "1.3.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",++ "integrity": "sha512-dIXLK5JpFqUEztywlWgSupUa9NEkovVczR2qeIvpst5evOyKZlwGzQktArvWUQn1ly6CoLL5mn/FnKiqoPvZFA==","license": "MIT","engines": {"node": ">=20"@@@ -25,7 +25,7 @@"express-rate-limit": "^7.5.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0","pg": "^8.23.0"@@@ -8,13 +8,16 @@* /feed.atom published summaries (Atom), /feed.json (JSON Feed 1.1); one entry per published* revision, so a correction is a new entry that starts with its note* /llms.txt orientation for language models++ * /llms-full.txt the same header plus a short summary of every indexable review page*/const express = require('express');const seo = require('openvibe-publishing/seo');const sharedSeo = require('openvibe-shared/seo');const cache = require('openvibe-shared/cache-policy');++const { SITE_SUMMARY } = require('../render/layout');const PER_SITEMAP = 45000;++const ALL_SUMMARIES = 50000;function createMachine({ svc, config }) {const router = express.Router();@@@ -76,7 +79,7 @@ function createMachine({ svc, config }) {router.get('/llms.txt', (_req, res) => {setCache(res, 3600).type('text/plain').send(sharedSeo.llmsTxt({name: 'OpenVibe.Reviews',-- summary: 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.',++ summary: SITE_SUMMARY,details: 'Every entity page has a JSON representation at the same address plus ".json" (same content). Each signal names its source item, retrieval time and licence note; the aggregate lists its inputs, exclusions and computation (method reviews-aggregate@1). Summaries never carry a rating; AI-drafted summaries are labelled and published only after a person approves them. Review text is not republished.',sections: [{ title: 'Start here', links: [{ title: 'Entities', url: `${origin}/` }, { title: 'How it works', url: `${origin}/about` }] },@@@ -89,6 +92,23 @@ function createMachine({ svc, config }) {}));});++ router.get('/llms-full.txt', async (_req, res) => {++ // The /llms.txt header plus a section of only indexable reviews — the same gate the sitemap++ // applies — each as its short summary. Review text is never stored, so no body can appear.++ const pages = (await svc.recentSummaries(ALL_SUMMARIES)).filter(({ decision }) => decision.indexable).map(({ entity, rev }) => ({++ title: entity.name,++ url: svc.entityUrl(entity),++ text: [rev.content || '', ...(rev.fields.pros || []).map((p) => p.text), ...(rev.fields.cons || []).map((p) => p.text)].join('\n').slice(0, 1000),++ }));++ setCache(res, 3600).type('text/plain').send(sharedSeo.llmsFull({++ site: 'OpenVibe.Reviews',++ summary: SITE_SUMMARY,++ base: origin,++ maxBytes: 512 * 1024,++ sections: [{ title: 'Reviews', pages }],++ }));++ });++return router;}@@@ -25,7 +25,7 @@ const ovServe = require('openvibe-shared/serve');const cache = require('openvibe-shared/cache-policy');const frame = require('openvibe-shared/frame');const seo = require('openvibe-publishing/seo');--const { renderPage } = require('../render/layout');++const { renderPage, SITE_SUMMARY } = require('../render/layout');const views = require('../render/views');const { ratingForStructuredData } = require('../reviews/aggregate');const { actorMiddleware, crossSite } = require('./common');@@@ -146,7 +146,7 @@ function createPages({ svc, platform, sync, viewers, config, log = console, limirouter.get('/', async (req, res) => {const all = await svc.listEntities({ limit: 1 });send(req, res, 200, views.home({ entities: await svc.entitiesWithData(60), total: all.total, editor: isEditor(req) }), {-- robots: 'index, follow', cache: 'public', active: 'home', path: '/',++ robots: 'index, follow', cache: 'public', active: 'home', path: '/', summary: SITE_SUMMARY,jsonLd: seo.structuredData.webPage({ url: `${config.baseUrl}/`, name: 'OpenVibe.Reviews', description: 'Review signals from named sources, with provenance and honest aggregates.' }),});});@@@ -16,6 +16,7 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');const SITE_NAME = 'OpenVibe.Reviews';const NETWORK_URL = 'https://openvibe.network';++const SITE_SUMMARY = 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.';const DEFAULT_DESCRIPTION = 'OpenVibe.Reviews: review signals gathered from named sources, with provenance for every number and no rating without source data. Part of the OpenVibe network.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');@@@ -78,6 +79,9 @@ function renderPage(o) {lang: 'en',title,description: o.description || DEFAULT_DESCRIPTION,++ // The one-line AI summary: on the home page it becomes the ai-summary meta and the WebPage++ // JSON-LD (openvibe-publishing 1.3.0 forwards it to the shell). Other pages leave it out.++ summary: o.summary,canonical: o.canonical || `${config.baseUrl}${o.path || '/'}`,decision: o.decision,robots: o.robots,@@@ -99,4 +103,4 @@ function renderPage(o) {});}--module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, NETWORK_URL };++module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, DEFAULT_DESCRIPTION, NETWORK_URL };@@@ -82,7 +82,9 @@ t('the document head and frame come from openvibe-publishing/layout, robots exacconst home = (await req(h, 'GET', '/')).text;const homeHead = home.slice(0, home.indexOf('</head>'));assert.ok(homeHead.includes('<meta name="robots" content="index, follow">'));-- assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 1, 'the home page JSON-LD');++ // The home carries its own WebPage JSON-LD and, since llms-full.txt, the site summary's WebPage JSON-LD.++ assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 2, 'the home page JSON-LD');++ assert.match(homeHead, /<meta name="ai-summary" content="[^"]+">/, 'the AI summary head');assert.ok(home.includes('Recently shipped on OpenVibe.Reviews'), 'the shipped line stays on the home page');} finally { await h.stop(); }});@@@ -118,6 +118,36 @@ t('every public page reads without JavaScript; sitemaps, feeds and robots follow} finally { await h.stop(); }});++t('/llms-full.txt carries only indexable reviews and the home page an AI summary', async () => {++ const h = await boot();++ try {++ const e = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });++ await importItem(h, steamItem({ votedUp: true }));++ const thin = await createEntity(h, { name: 'Thin Widget', kind: 'product' });++ const sig = await h.db.prepare('SELECT id FROM review_signals').all();++ const pub = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/revisions`, { token: editorToken(), body: {++ overview: 'One recent review recommends it, which is the whole sample Reviews has read so far and says nothing about the game beyond that.',++ overview_signals: sig.map((s) => s.id),++ publish: true,++ } });++ assert.strictEqual(pub.status, 201, pub.text.slice(0, 300));++++ const full = await req(h, 'GET', '/llms-full.txt');++ assert.strictEqual(full.status, 200);++ assert.match(full.headers.get('content-type'), /^text\/plain/);++ assert.strictEqual(full.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');++ assert.ok(full.text.includes('OpenVibe.Reviews'), 'the llms.txt header');++ assert.ok(full.text.includes(`http://reviews.test/e/${e.slug}`), 'an indexable review is listed');++ assert.match(full.text, /One recent review recommends it/, 'its short summary is the text');++ assert.ok(!full.text.includes(`/e/${thin.slug}`), 'a thin, non-indexable review is never listed');++ assert.ok(Buffer.byteLength(full.text) <= 512 * 1024, 'stays within maxBytes');++++ const home = await req(h, 'GET', '/');++ assert.match(home.text, /<meta name="ai-summary" content="Review signals from named sources/);++ assert.match(home.text, /"@type":"WebPage"/, 'the home WebPage JSON-LD');++ } finally { await h.stop(); }++});++t('editors work with plain forms: create, resolve an ambiguous item, summarise, merge, split, correct', async () => {const h = await boot();try {
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.reviews",
"domain": "openvibe.reviews",
"contracts": "openvibe-contracts v0.76.0",
"contracts": "openvibe-contracts v0.76.0",
"packages": [
"packages": [
- "openvibe-publishing v1.2.0",
+ "openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
"openvibe-sdk v0.26.0"
"openvibe-sdk v0.26.0"
],
],
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
+ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1149,9 +1149,9 @@
@@ -1149,9 +1149,9 @@
}
}
},
},
"node_modules/openvibe-publishing": {
"node_modules/openvibe-publishing": {
- "version": "1.2.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
- "integrity": "sha512-n/20yxXo5jRs0wi7KsR+R9jDkGsoBak3gqElHV6XyU+i1t3XCzaoz+sDb10G2cbc4uEaNB/F09VtQKrQzLp6YQ==",
+ "version": "1.3.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
+ "integrity": "sha512-dIXLK5JpFqUEztywlWgSupUa9NEkovVczR2qeIvpst5evOyKZlwGzQktArvWUQn1ly6CoLL5mn/FnKiqoPvZFA==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
+ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -8,13 +8,16 @@
@@ -8,13 +8,16 @@
* /feed.atom published summaries (Atom), /feed.json (JSON Feed 1.1); one entry per published
* /feed.atom published summaries (Atom), /feed.json (JSON Feed 1.1); one entry per published
* revision, so a correction is a new entry that starts with its note
* revision, so a correction is a new entry that starts with its note
* /llms.txt orientation for language models
* /llms.txt orientation for language models
+ * /llms-full.txt the same header plus a short summary of every indexable review page
*/
*/
const express = require('express');
const express = require('express');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
const sharedSeo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
+const { SITE_SUMMARY } = require('../render/layout');
const PER_SITEMAP = 45000;
const PER_SITEMAP = 45000;
+const ALL_SUMMARIES = 50000;
function createMachine({ svc, config }) {
function createMachine({ svc, config }) {
const router = express.Router();
const router = express.Router();
@@ -76,7 +79,7 @@ function createMachine({ svc, config }) {
@@ -76,7 +79,7 @@ function createMachine({ svc, config }) {
router.get('/llms.txt', (_req, res) => {
router.get('/llms.txt', (_req, res) => {
setCache(res, 3600).type('text/plain').send(sharedSeo.llmsTxt({
setCache(res, 3600).type('text/plain').send(sharedSeo.llmsTxt({
name: 'OpenVibe.Reviews',
name: 'OpenVibe.Reviews',
- summary: 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.',
+ summary: SITE_SUMMARY,
details: 'Every entity page has a JSON representation at the same address plus ".json" (same content). Each signal names its source item, retrieval time and licence note; the aggregate lists its inputs, exclusions and computation (method reviews-aggregate@1). Summaries never carry a rating; AI-drafted summaries are labelled and published only after a person approves them. Review text is not republished.',
details: 'Every entity page has a JSON representation at the same address plus ".json" (same content). Each signal names its source item, retrieval time and licence note; the aggregate lists its inputs, exclusions and computation (method reviews-aggregate@1). Summaries never carry a rating; AI-drafted summaries are labelled and published only after a person approves them. Review text is not republished.',
sections: [
sections: [
{ title: 'Start here', links: [{ title: 'Entities', url: `${origin}/` }, { title: 'How it works', url: `${origin}/about` }] },
{ title: 'Start here', links: [{ title: 'Entities', url: `${origin}/` }, { title: 'How it works', url: `${origin}/about` }] },
@@ -89,6 +92,23 @@ function createMachine({ svc, config }) {
@@ -89,6 +92,23 @@ function createMachine({ svc, config }) {
}));
}));
});
});
+ router.get('/llms-full.txt', async (_req, res) => {
+ // The /llms.txt header plus a section of only indexable reviews — the same gate the sitemap
+ // applies — each as its short summary. Review text is never stored, so no body can appear.
+ const pages = (await svc.recentSummaries(ALL_SUMMARIES)).filter(({ decision }) => decision.indexable).map(({ entity, rev }) => ({
+ title: entity.name,
+ url: svc.entityUrl(entity),
+ text: [rev.content || '', ...(rev.fields.pros || []).map((p) => p.text), ...(rev.fields.cons || []).map((p) => p.text)].join('\n').slice(0, 1000),
+ }));
+ setCache(res, 3600).type('text/plain').send(sharedSeo.llmsFull({
+ site: 'OpenVibe.Reviews',
+ summary: SITE_SUMMARY,
+ base: origin,
+ maxBytes: 512 * 1024,
+ sections: [{ title: 'Reviews', pages }],
+ }));
+ });
+
return router;
return router;
}
}
@@ -25,7 +25,7 @@ const ovServe = require('openvibe-shared/serve');
@@ -25,7 +25,7 @@ const ovServe = require('openvibe-shared/serve');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
-const { renderPage } = require('../render/layout');
+const { renderPage, SITE_SUMMARY } = require('../render/layout');
const views = require('../render/views');
const views = require('../render/views');
const { ratingForStructuredData } = require('../reviews/aggregate');
const { ratingForStructuredData } = require('../reviews/aggregate');
const { actorMiddleware, crossSite } = require('./common');
const { actorMiddleware, crossSite } = require('./common');
@@ -146,7 +146,7 @@ function createPages({ svc, platform, sync, viewers, config, log = console, limi
@@ -146,7 +146,7 @@ function createPages({ svc, platform, sync, viewers, config, log = console, limi
router.get('/', async (req, res) => {
router.get('/', async (req, res) => {
const all = await svc.listEntities({ limit: 1 });
const all = await svc.listEntities({ limit: 1 });
send(req, res, 200, views.home({ entities: await svc.entitiesWithData(60), total: all.total, editor: isEditor(req) }), {
send(req, res, 200, views.home({ entities: await svc.entitiesWithData(60), total: all.total, editor: isEditor(req) }), {
- robots: 'index, follow', cache: 'public', active: 'home', path: '/',
+ robots: 'index, follow', cache: 'public', active: 'home', path: '/', summary: SITE_SUMMARY,
jsonLd: seo.structuredData.webPage({ url: `${config.baseUrl}/`, name: 'OpenVibe.Reviews', description: 'Review signals from named sources, with provenance and honest aggregates.' }),
jsonLd: seo.structuredData.webPage({ url: `${config.baseUrl}/`, name: 'OpenVibe.Reviews', description: 'Review signals from named sources, with provenance and honest aggregates.' }),
});
});
});
});
@@ -16,6 +16,7 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
@@ -16,6 +16,7 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
const SITE_NAME = 'OpenVibe.Reviews';
const SITE_NAME = 'OpenVibe.Reviews';
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
+const SITE_SUMMARY = 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.';
const DEFAULT_DESCRIPTION = 'OpenVibe.Reviews: review signals gathered from named sources, with provenance for every number and no rating without source data. Part of the OpenVibe network.';
const DEFAULT_DESCRIPTION = 'OpenVibe.Reviews: review signals gathered from named sources, with provenance for every number and no rating without source data. Part of the OpenVibe network.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
@@ -78,6 +79,9 @@ function renderPage(o) {
@@ -78,6 +79,9 @@ function renderPage(o) {
lang: 'en',
lang: 'en',
title,
title,
description: o.description || DEFAULT_DESCRIPTION,
description: o.description || DEFAULT_DESCRIPTION,
+ // The one-line AI summary: on the home page it becomes the ai-summary meta and the WebPage
+ // JSON-LD (openvibe-publishing 1.3.0 forwards it to the shell). Other pages leave it out.
+ summary: o.summary,
canonical: o.canonical || `${config.baseUrl}${o.path || '/'}`,
canonical: o.canonical || `${config.baseUrl}${o.path || '/'}`,
decision: o.decision,
decision: o.decision,
robots: o.robots,
robots: o.robots,
@@ -99,4 +103,4 @@ function renderPage(o) {
@@ -99,4 +103,4 @@ function renderPage(o) {
});
});
}
}
-module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, NETWORK_URL };
+module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, DEFAULT_DESCRIPTION, NETWORK_URL };
@@ -82,7 +82,9 @@ t('the document head and frame come from openvibe-publishing/layout, robots exac
@@ -82,7 +82,9 @@ t('the document head and frame come from openvibe-publishing/layout, robots exac
const home = (await req(h, 'GET', '/')).text;
const home = (await req(h, 'GET', '/')).text;
const homeHead = home.slice(0, home.indexOf('</head>'));
const homeHead = home.slice(0, home.indexOf('</head>'));
assert.ok(homeHead.includes('<meta name="robots" content="index, follow">'));
assert.ok(homeHead.includes('<meta name="robots" content="index, follow">'));
- assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 1, 'the home page JSON-LD');
+ // The home carries its own WebPage JSON-LD and, since llms-full.txt, the site summary's WebPage JSON-LD.
+ assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 2, 'the home page JSON-LD');
+ assert.match(homeHead, /<meta name="ai-summary" content="[^"]+">/, 'the AI summary head');
assert.ok(home.includes('Recently shipped on OpenVibe.Reviews'), 'the shipped line stays on the home page');
assert.ok(home.includes('Recently shipped on OpenVibe.Reviews'), 'the shipped line stays on the home page');
} finally { await h.stop(); }
} finally { await h.stop(); }
});
});
@@ -118,6 +118,36 @@ t('every public page reads without JavaScript; sitemaps, feeds and robots follow
@@ -118,6 +118,36 @@ t('every public page reads without JavaScript; sitemaps, feeds and robots follow
} finally { await h.stop(); }
} finally { await h.stop(); }
});
});
+t('/llms-full.txt carries only indexable reviews and the home page an AI summary', async () => {
+ const h = await boot();
+ try {
+ const e = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });
+ await importItem(h, steamItem({ votedUp: true }));
+ const thin = await createEntity(h, { name: 'Thin Widget', kind: 'product' });
+ const sig = await h.db.prepare('SELECT id FROM review_signals').all();
+ const pub = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/revisions`, { token: editorToken(), body: {
+ overview: 'One recent review recommends it, which is the whole sample Reviews has read so far and says nothing about the game beyond that.',
+ overview_signals: sig.map((s) => s.id),
+ publish: true,
+ } });
+ assert.strictEqual(pub.status, 201, pub.text.slice(0, 300));
+
+ const full = await req(h, 'GET', '/llms-full.txt');
+ assert.strictEqual(full.status, 200);
+ assert.match(full.headers.get('content-type'), /^text\/plain/);
+ assert.strictEqual(full.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
+ assert.ok(full.text.includes('OpenVibe.Reviews'), 'the llms.txt header');
+ assert.ok(full.text.includes(`http://reviews.test/e/${e.slug}`), 'an indexable review is listed');
+ assert.match(full.text, /One recent review recommends it/, 'its short summary is the text');
+ assert.ok(!full.text.includes(`/e/${thin.slug}`), 'a thin, non-indexable review is never listed');
+ assert.ok(Buffer.byteLength(full.text) <= 512 * 1024, 'stays within maxBytes');
+
+ const home = await req(h, 'GET', '/');
+ assert.match(home.text, /<meta name="ai-summary" content="Review signals from named sources/);
+ assert.match(home.text, /"@type":"WebPage"/, 'the home WebPage JSON-LD');
+ } finally { await h.stop(); }
+});
+
t('editors work with plain forms: create, resolve an ambiguous item, summarise, merge, split, correct', async () => {
t('editors work with plain forms: create, resolve an ambiguous item, summarise, merge, split, correct', async () => {
const h = await boot();
const h = await boot();
try {
try {