openvibers/openvibe.blog Commits
Add /llms-full.txt and the home AI summary, bump openvibe-publishing…
Changes
@@@ -9,7 +9,7 @@"domain": "openvibe.blog","contracts": "openvibe-contracts v0.79.0","packages": [-- "openvibe-publishing v1.2.0",++ "openvibe-publishing v1.3.0","openvibe-contracts v0.79.0","openvibe-shared v2.6.0","openvibe-sdk v0.26.0"@@@ -16,7 +16,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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"@@@ -889,9 +889,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"@@@ -28,7 +28,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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"@@@ -5,6 +5,7 @@** GET /robots.txt sitemap location + explicit automated-consumer policy* GET /llms.txt orientation for language models (what the site is, entry points)++ * GET /llms-full.txt the same header plus an excerpt of every indexable post (never the body)* GET /sitemap.xml sitemap index over the two sections below* GET /sitemaps/posts.xml published, public, INDEXABLE posts only (the gate decides), lastmod* = the published revision's real time@@@ -15,8 +16,10 @@*/const express = require('express');const seo = require('openvibe-publishing/seo');++const ssr = require('openvibe-publishing/ssr');const sharedSeo = require('openvibe-shared/seo');const cache = require('openvibe-shared/cache-policy');++const { SITE_SUMMARY } = require('../render/layout');function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {const router = express.Router();@@@ -57,7 +60,7 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {const official = await blogs.official();res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({name: 'OpenVibe.Blog',-- summary: 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.',++ summary: SITE_SUMMARY,details: 'Only public, published posts are listed, fed or mapped. Every post page has a machine-readable twin at <post URL>.json with the same content, its revision, authorship (human, AI-assisted or AI-generated, and whether a person reviewed it), sources and indexability reasons. AI-generated drafts are never published or indexed before a person reviews them.',sections: [{ title: 'Start here', links: [@@@ -73,6 +76,23 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {}));});++ router.get('/llms-full.txt', async (_req, res) => {++ // Only public, published, indexable posts (the sitemap's gate), each as its summary — or a++ // clipped excerpt when the author wrote no summary. Full post bodies are never included.++ const posts = (await postEntries()).filter((e) => e.decision.indexable).map((e) => ({++ title: e.post.title,++ url: publication.postUrl(e.blog, e.post),++ text: e.rev.fields.summary || ssr.markdownToText(e.rev.content || '', 160),++ }));++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsFull({++ site: 'OpenVibe.Blog',++ summary: SITE_SUMMARY,++ base: config.baseUrl,++ maxBytes: 512 * 1024,++ sections: [{ title: 'Posts', pages: posts }],++ }));++ });++router.get('/sitemap.xml', async (_req, res) => {const entries = (await postEntries()).filter((e) => e.decision.indexable);const newest = entries.length ? entries.map((e) => e.rev.createdAt).sort().pop() : null;@@@ -24,7 +24,7 @@ const seo = require('openvibe-publishing/seo');const ssr = require('openvibe-publishing/ssr');const authorship = require('openvibe-publishing/authorship');const cache = require('openvibe-shared/cache-policy');--const { renderPage } = require('../render/layout');++const { renderPage, SITE_SUMMARY } = require('../render/layout');const pages = require('../render/pages');const { csrfToken, checkCsrf } = require('../auth/forms');@@@ -127,6 +127,8 @@ function createPublicRoutes(ctx) {send(req, res, 200, {title: blog.title,description: blog.description,++ // The network blog's home carries the site summary an AI can read (ai-summary meta + WebPage JSON-LD).++ ...(home ? { summary: SITE_SUMMARY } : {}),decision: pageDecision(canonical, { empty: total === 0, query: ['page'] }),canonical,feeds,@@@ -19,6 +19,8 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');const NETWORK_URL = 'https://openvibe.network';const SITE_NAME = 'OpenVibe.Blog';++// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.++const SITE_SUMMARY = 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');const hashes = new Map();@@@ -80,6 +82,10 @@ function renderPage(o) {description: o.description || 'The official OpenVibe blog and a blog for every member.',canonical: o.canonical,decision: o.decision,++ summary: o.summary,++ facts: o.facts,++ updated: o.updated,++ url: o.url,type: o.type || 'website',image: o.image,author: o.author,@@@ -106,4 +112,4 @@ function renderPage(o) {return style ? html.replace('<main id="main" class="page blog-surface">', () => `<main id="main" class="page blog-surface"${style}>`) : html;}--module.exports = { renderPage, asset, assetVersion, themeStyle, setRelease, SITE_NAME, NETWORK_URL };++module.exports = { renderPage, asset, assetVersion, themeStyle, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL };@@@ -56,14 +56,16 @@ const split = (html) => ({ head: html.slice(0, html.indexOf('</head>')), body: hassert.strictEqual(count(r.text, /<main\b/g), 1, 'one <main>');});-- await check('the editor stays noindex; the home page keeps its feeds and one JSON-LD', async () => {++ await check('the editor stays noindex; the home page keeps its feeds, the Blog JSON-LD and the AI summary', async () => {const { head } = split((await t.get('/write', { as: eve })).text);assert.strictEqual(count(head, /<title>/g), 1);assert.ok(/<meta name="robots" content="noindex[^"]*">/.test(head), 'the editor is noindex');const home = split((await t.get('/')).text).head;assert.strictEqual(count(home, /<title>/g), 1);assert.ok(home.includes('<link rel="canonical" href="https://openvibe.blog/">'));-- assert.strictEqual(count(home, /<script type="application\/ld\+json">/g), 1, 'one JSON-LD script on the home page');++ // The home carries the Blog JSON-LD and, since llms-full.txt, the site summary's WebPage JSON-LD.++ assert.deepStrictEqual([...home.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/g)].map((m) => JSON.parse(m[1])['@type']), ['Blog', 'WebPage']);++ assert.match(home, /<meta name="ai-summary" content="[^"]+">/, 'the AI summary head');assert.ok(home.includes('<link rel="alternate" type="application/atom+xml" href="/atom.xml" title="The OpenVibe blog (Atom)">'));assert.ok(home.includes('<link rel="alternate" type="application/feed+json" href="/feed.json" title="The OpenVibe blog (JSON Feed)">'));assert.ok(/<link rel="stylesheet" href="\/shared\/showcase\.css\?v=[0-9a-f]+">/.test(home), 'the showcase kit stylesheet on the home');@@@ -82,6 +82,29 @@ const { seedOfficialPost, loadFacts, SLUG } = require('../server/seed');assert.match(idx, /sitemaps\/posts\.xml/);});++ await check('llms-full.txt lists only indexable posts as summary text, within maxBytes, never the body', async () => {++ const full = await t.get('/llms-full.txt');++ assert.strictEqual(full.status, 200);++ assert.match(full.headers.get('content-type'), /text\/plain/);++ assert.ok(Buffer.byteLength(full.text) <= 512 * 1024, 'stays within maxBytes (512 KiB)');++ assert.match(full.text, /^# OpenVibe\.Blog/);++ const urls = [...full.text.matchAll(/^URL: (\S+)$/gm)].map((m) => m[1]);++ assert.ok(urls.length >= 1, 'lists the published post');++ // The same gate as the sitemap: every listed URL is one the sitemap also lists as indexable.++ const locs = new Set([...(await t.get('/sitemaps/posts.xml')).text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]));++ for (const u of urls) assert.ok(locs.has(u), `only indexable posts: ${u}`);++ // The author's summary, not the full body (the verbatim quotes live only in the post).++ assert.match(full.text, /Commit messages and release notes quoted from the OpenVibers repositories/);++ assert.ok(!full.text.includes(facts.items[0].quote), 'the body is never included');++ });++++ await check('/ carries the AI summary head (ai-summary meta and WebPage JSON-LD)', async () => {++ const home = await t.get('/');++ assert.match(home.text, /<meta name="ai-summary" content="[^"]+">/);++ const ld = [...home.text.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/g)].map((m) => JSON.parse(m[1]));++ assert.ok(ld.some((x) => x['@type'] === 'WebPage' && x.description && x.url === 'https://openvibe.blog/'), 'WebPage JSON-LD names the home URL and summary');++ });++await check('legal pages and 404s', async () => {assert.strictEqual((await t.get('/terms')).status, 200);const nf = await t.get('/@nobody-here');
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.blog",
"domain": "openvibe.blog",
"contracts": "openvibe-contracts v0.79.0",
"contracts": "openvibe-contracts v0.79.0",
"packages": [
"packages": [
- "openvibe-publishing v1.2.0",
+ "openvibe-publishing v1.3.0",
"openvibe-contracts v0.79.0",
"openvibe-contracts v0.79.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
"openvibe-sdk v0.26.0"
"openvibe-sdk v0.26.0"
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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"
@@ -889,9 +889,9 @@
@@ -889,9 +889,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"
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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"
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
*
*
* GET /robots.txt sitemap location + explicit automated-consumer policy
* GET /robots.txt sitemap location + explicit automated-consumer policy
* GET /llms.txt orientation for language models (what the site is, entry points)
* GET /llms.txt orientation for language models (what the site is, entry points)
+ * GET /llms-full.txt the same header plus an excerpt of every indexable post (never the body)
* GET /sitemap.xml sitemap index over the two sections below
* GET /sitemap.xml sitemap index over the two sections below
* GET /sitemaps/posts.xml published, public, INDEXABLE posts only (the gate decides), lastmod
* GET /sitemaps/posts.xml published, public, INDEXABLE posts only (the gate decides), lastmod
* = the published revision's real time
* = the published revision's real time
@@ -15,8 +16,10 @@
@@ -15,8 +16,10 @@
*/
*/
const express = require('express');
const express = require('express');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
+const ssr = require('openvibe-publishing/ssr');
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');
function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
const router = express.Router();
const router = express.Router();
@@ -57,7 +60,7 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
@@ -57,7 +60,7 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
const official = await blogs.official();
const official = await blogs.official();
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({
name: 'OpenVibe.Blog',
name: 'OpenVibe.Blog',
- summary: 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.',
+ summary: SITE_SUMMARY,
details: 'Only public, published posts are listed, fed or mapped. Every post page has a machine-readable twin at <post URL>.json with the same content, its revision, authorship (human, AI-assisted or AI-generated, and whether a person reviewed it), sources and indexability reasons. AI-generated drafts are never published or indexed before a person reviews them.',
details: 'Only public, published posts are listed, fed or mapped. Every post page has a machine-readable twin at <post URL>.json with the same content, its revision, authorship (human, AI-assisted or AI-generated, and whether a person reviewed it), sources and indexability reasons. AI-generated drafts are never published or indexed before a person reviews them.',
sections: [
sections: [
{ title: 'Start here', links: [
{ title: 'Start here', links: [
@@ -73,6 +76,23 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
@@ -73,6 +76,23 @@ function createDiscoveryRoutes({ config, store, blogs, publication, reading }) {
}));
}));
});
});
+ router.get('/llms-full.txt', async (_req, res) => {
+ // Only public, published, indexable posts (the sitemap's gate), each as its summary — or a
+ // clipped excerpt when the author wrote no summary. Full post bodies are never included.
+ const posts = (await postEntries()).filter((e) => e.decision.indexable).map((e) => ({
+ title: e.post.title,
+ url: publication.postUrl(e.blog, e.post),
+ text: e.rev.fields.summary || ssr.markdownToText(e.rev.content || '', 160),
+ }));
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsFull({
+ site: 'OpenVibe.Blog',
+ summary: SITE_SUMMARY,
+ base: config.baseUrl,
+ maxBytes: 512 * 1024,
+ sections: [{ title: 'Posts', pages: posts }],
+ }));
+ });
+
router.get('/sitemap.xml', async (_req, res) => {
router.get('/sitemap.xml', async (_req, res) => {
const entries = (await postEntries()).filter((e) => e.decision.indexable);
const entries = (await postEntries()).filter((e) => e.decision.indexable);
const newest = entries.length ? entries.map((e) => e.rev.createdAt).sort().pop() : null;
const newest = entries.length ? entries.map((e) => e.rev.createdAt).sort().pop() : null;
@@ -24,7 +24,7 @@ const seo = require('openvibe-publishing/seo');
@@ -24,7 +24,7 @@ const seo = require('openvibe-publishing/seo');
const ssr = require('openvibe-publishing/ssr');
const ssr = require('openvibe-publishing/ssr');
const authorship = require('openvibe-publishing/authorship');
const authorship = require('openvibe-publishing/authorship');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
-const { renderPage } = require('../render/layout');
+const { renderPage, SITE_SUMMARY } = require('../render/layout');
const pages = require('../render/pages');
const pages = require('../render/pages');
const { csrfToken, checkCsrf } = require('../auth/forms');
const { csrfToken, checkCsrf } = require('../auth/forms');
@@ -127,6 +127,8 @@ function createPublicRoutes(ctx) {
@@ -127,6 +127,8 @@ function createPublicRoutes(ctx) {
send(req, res, 200, {
send(req, res, 200, {
title: blog.title,
title: blog.title,
description: blog.description,
description: blog.description,
+ // The network blog's home carries the site summary an AI can read (ai-summary meta + WebPage JSON-LD).
+ ...(home ? { summary: SITE_SUMMARY } : {}),
decision: pageDecision(canonical, { empty: total === 0, query: ['page'] }),
decision: pageDecision(canonical, { empty: total === 0, query: ['page'] }),
canonical,
canonical,
feeds,
feeds,
@@ -19,6 +19,8 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
@@ -19,6 +19,8 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Blog';
const SITE_NAME = 'OpenVibe.Blog';
+// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.
+const SITE_SUMMARY = 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const hashes = new Map();
const hashes = new Map();
@@ -80,6 +82,10 @@ function renderPage(o) {
@@ -80,6 +82,10 @@ function renderPage(o) {
description: o.description || 'The official OpenVibe blog and a blog for every member.',
description: o.description || 'The official OpenVibe blog and a blog for every member.',
canonical: o.canonical,
canonical: o.canonical,
decision: o.decision,
decision: o.decision,
+ summary: o.summary,
+ facts: o.facts,
+ updated: o.updated,
+ url: o.url,
type: o.type || 'website',
type: o.type || 'website',
image: o.image,
image: o.image,
author: o.author,
author: o.author,
@@ -106,4 +112,4 @@ function renderPage(o) {
@@ -106,4 +112,4 @@ function renderPage(o) {
return style ? html.replace('<main id="main" class="page blog-surface">', () => `<main id="main" class="page blog-surface"${style}>`) : html;
return style ? html.replace('<main id="main" class="page blog-surface">', () => `<main id="main" class="page blog-surface"${style}>`) : html;
}
}
-module.exports = { renderPage, asset, assetVersion, themeStyle, setRelease, SITE_NAME, NETWORK_URL };
+module.exports = { renderPage, asset, assetVersion, themeStyle, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL };
@@ -56,14 +56,16 @@ const split = (html) => ({ head: html.slice(0, html.indexOf('</head>')), body: h
@@ -56,14 +56,16 @@ const split = (html) => ({ head: html.slice(0, html.indexOf('</head>')), body: h
assert.strictEqual(count(r.text, /<main\b/g), 1, 'one <main>');
assert.strictEqual(count(r.text, /<main\b/g), 1, 'one <main>');
});
});
- await check('the editor stays noindex; the home page keeps its feeds and one JSON-LD', async () => {
+ await check('the editor stays noindex; the home page keeps its feeds, the Blog JSON-LD and the AI summary', async () => {
const { head } = split((await t.get('/write', { as: eve })).text);
const { head } = split((await t.get('/write', { as: eve })).text);
assert.strictEqual(count(head, /<title>/g), 1);
assert.strictEqual(count(head, /<title>/g), 1);
assert.ok(/<meta name="robots" content="noindex[^"]*">/.test(head), 'the editor is noindex');
assert.ok(/<meta name="robots" content="noindex[^"]*">/.test(head), 'the editor is noindex');
const home = split((await t.get('/')).text).head;
const home = split((await t.get('/')).text).head;
assert.strictEqual(count(home, /<title>/g), 1);
assert.strictEqual(count(home, /<title>/g), 1);
assert.ok(home.includes('<link rel="canonical" href="https://openvibe.blog/">'));
assert.ok(home.includes('<link rel="canonical" href="https://openvibe.blog/">'));
- assert.strictEqual(count(home, /<script type="application\/ld\+json">/g), 1, 'one JSON-LD script on the home page');
+ // The home carries the Blog JSON-LD and, since llms-full.txt, the site summary's WebPage JSON-LD.
+ assert.deepStrictEqual([...home.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/g)].map((m) => JSON.parse(m[1])['@type']), ['Blog', 'WebPage']);
+ assert.match(home, /<meta name="ai-summary" content="[^"]+">/, 'the AI summary head');
assert.ok(home.includes('<link rel="alternate" type="application/atom+xml" href="/atom.xml" title="The OpenVibe blog (Atom)">'));
assert.ok(home.includes('<link rel="alternate" type="application/atom+xml" href="/atom.xml" title="The OpenVibe blog (Atom)">'));
assert.ok(home.includes('<link rel="alternate" type="application/feed+json" href="/feed.json" title="The OpenVibe blog (JSON Feed)">'));
assert.ok(home.includes('<link rel="alternate" type="application/feed+json" href="/feed.json" title="The OpenVibe blog (JSON Feed)">'));
assert.ok(/<link rel="stylesheet" href="\/shared\/showcase\.css\?v=[0-9a-f]+">/.test(home), 'the showcase kit stylesheet on the home');
assert.ok(/<link rel="stylesheet" href="\/shared\/showcase\.css\?v=[0-9a-f]+">/.test(home), 'the showcase kit stylesheet on the home');
@@ -82,6 +82,29 @@ const { seedOfficialPost, loadFacts, SLUG } = require('../server/seed');
@@ -82,6 +82,29 @@ const { seedOfficialPost, loadFacts, SLUG } = require('../server/seed');
assert.match(idx, /sitemaps\/posts\.xml/);
assert.match(idx, /sitemaps\/posts\.xml/);
});
});
+ await check('llms-full.txt lists only indexable posts as summary text, within maxBytes, never the body', async () => {
+ const full = await t.get('/llms-full.txt');
+ assert.strictEqual(full.status, 200);
+ assert.match(full.headers.get('content-type'), /text\/plain/);
+ assert.ok(Buffer.byteLength(full.text) <= 512 * 1024, 'stays within maxBytes (512 KiB)');
+ assert.match(full.text, /^# OpenVibe\.Blog/);
+ const urls = [...full.text.matchAll(/^URL: (\S+)$/gm)].map((m) => m[1]);
+ assert.ok(urls.length >= 1, 'lists the published post');
+ // The same gate as the sitemap: every listed URL is one the sitemap also lists as indexable.
+ const locs = new Set([...(await t.get('/sitemaps/posts.xml')).text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]));
+ for (const u of urls) assert.ok(locs.has(u), `only indexable posts: ${u}`);
+ // The author's summary, not the full body (the verbatim quotes live only in the post).
+ assert.match(full.text, /Commit messages and release notes quoted from the OpenVibers repositories/);
+ assert.ok(!full.text.includes(facts.items[0].quote), 'the body is never included');
+ });
+
+ await check('/ carries the AI summary head (ai-summary meta and WebPage JSON-LD)', async () => {
+ const home = await t.get('/');
+ assert.match(home.text, /<meta name="ai-summary" content="[^"]+">/);
+ const ld = [...home.text.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
+ assert.ok(ld.some((x) => x['@type'] === 'WebPage' && x.description && x.url === 'https://openvibe.blog/'), 'WebPage JSON-LD names the home URL and summary');
+ });
+
await check('legal pages and 404s', async () => {
await check('legal pages and 404s', async () => {
assert.strictEqual((await t.get('/terms')).status, 200);
assert.strictEqual((await t.get('/terms')).status, 200);
const nf = await t.get('/@nobody-here');
const nf = await t.get('/@nobody-here');