openvibers/openvibe.shared Commits
4.108.1: a page named after a domain keeps its Markdown twin in llms.txt (#284)
Changes
@@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## 4.108.1 — 2026-11-03++++- **A page named after a domain keeps its Markdown twin in llms.txt.** 4.108.0 counted any last segment with a dot as a++ file, so Help's `/sites/openvibe.blog` and Host's `/catalog/domains/co.uk` kept their HTML URLs.++ `/sites/openvibe.community` got its twin only because ".community" is longer than eight letters. Now only a real++ file ending (json, xml, txt, png, html, …) makes a file. `test/llms-markdown-links.test.js`.++## 4.108.0 — 2026-11-03- **llms.txt can link a site's own pages at their Markdown twins, and only those** (AGT-5). `seo.llmsTxt({ markdown:@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.108.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.108.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.108.0",++ "version": "4.108.1","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.108.0",++ "version": "4.108.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.108.0",++ "version": "4.108.1","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.108.0",++ "version": "4.108.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -276,9 +276,16 @@ function robotsTxt({ sitemaps = [], disallow = [], allow = ['/'], allowAI = truereturn lines.join('\n') + '\n';}++// The endings that make a path a file rather than a page (4.108.1). Any dot used to count, so a page named after a++// domain (Help's /sites/openvibe.blog, Host's /catalog/domains/co.uk) lost its twin, unless its ending was longer++// than eight letters (/sites/openvibe.community kept it).++const FILE_EXT = new Set(('txt md json jsonl xml rss atom csv tsv yaml yml toml ics vcf webmanifest html htm js mjs cjs css map wasm '++ + 'png jpg jpeg gif webp avif svg ico bmp tif tiff heic pdf zip gz tgz tar bz2 xz 7z rar dmg exe msi deb rpm apk '++ + 'mp3 mp4 m4a m4v webm ogg oga ogv wav flac mov avi mkv m3u8 ts srt vtt woff woff2 ttf otf eot sh ps1 py').split(' '));++/*** Whether llms.txt's markdown mode links `url` at its Markdown twin: one of the site's own pages. A file (a last path-- * segment with an extension: /openapi.json, /feed.xml, /llms-full.txt), an /api/ or /.well-known/ path and, when++ * segment with a file ending: /openapi.json, /feed.xml, /llms-full.txt), an /api/ or /.well-known/ path and, when* `base` is given, another origin's link keep their URL (4.108.0; before, /openapi.json became /openapi.json.md).*/function twinnable(url, base) {@@@ -287,7 +294,9 @@ function twinnable(url, base) {if (!/^https?:$/.test(u.protocol)) return false;if (base && /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(String(url)) && u.origin !== new URL(base).origin) return false;if (/^\/(api|\.well-known)(\/|$)/.test(u.pathname)) return false;-- return !/\.[a-z0-9]{1,8}$/i.test(u.pathname.split('/').pop());++ const last = u.pathname.split('/').pop();++ const dot = last.lastIndexOf('.');++ return dot < 0 || !FILE_EXT.has(last.slice(dot + 1).toLowerCase());}/**@@@ -36,4 +36,11 @@ const old = seo.llmsTxt({ name: 'X', markdown: true, sections: [{ title: 'P', liassert.deepStrictEqual(links(old), ['https://example.com/index.md', 'https://example.com/openapi.json']);// Without markdown nothing changes.assert.deepStrictEqual(links(seo.llmsTxt({ name: 'X', sections: [{ title: 'P', links: [{ title: 'Hot', url: '/hot' }] }] })), ['/hot']);++// 4.108.1: a page named after a domain is a page; only a real file ending makes a file.++const domains = seo.llmsTxt({ name: 'Help', markdown: true, base: 'https://openvibe.help', sections: [{ title: 'Sites', links: [++ { title: 'Blog', url: 'https://openvibe.help/sites/openvibe.blog' }, { title: 'Community', url: 'https://openvibe.help/sites/openvibe.community' },++ { title: 'UK', url: '/catalog/domains/co.uk' }, { title: 'Data', url: '/export.CSV' }, { title: 'Old page', url: '/legacy/index.html' }, { title: 'v2.1', url: '/releases/v2.1' },++] }] });++assert.deepStrictEqual(links(domains), ['https://openvibe.help/sites/openvibe.blog.md', 'https://openvibe.help/sites/openvibe.community.md',++ '/catalog/domains/co.uk.md', '/export.CSV', '/legacy/index.html', '/releases/v2.1.md']);console.log('llms markdown links: ok');
@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 4.108.1 — 2026-11-03
+
+- **A page named after a domain keeps its Markdown twin in llms.txt.** 4.108.0 counted any last segment with a dot as a
+ file, so Help's `/sites/openvibe.blog` and Host's `/catalog/domains/co.uk` kept their HTML URLs.
+ `/sites/openvibe.community` got its twin only because ".community" is longer than eight letters. Now only a real
+ file ending (json, xml, txt, png, html, …) makes a file. `test/llms-markdown-links.test.js`.
+
## 4.108.0 — 2026-11-03
## 4.108.0 — 2026-11-03
- **llms.txt can link a site's own pages at their Markdown twins, and only those** (AGT-5). `seo.llmsTxt({ markdown:
- **llms.txt can link a site's own pages at their Markdown twins, and only those** (AGT-5). `seo.llmsTxt({ markdown:
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.108.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.108.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.108.0",
+ "version": "4.108.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.108.0",
+ "version": "4.108.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.108.0",
+ "version": "4.108.1",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.108.0",
+ "version": "4.108.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -276,9 +276,16 @@ function robotsTxt({ sitemaps = [], disallow = [], allow = ['/'], allowAI = true
@@ -276,9 +276,16 @@ function robotsTxt({ sitemaps = [], disallow = [], allow = ['/'], allowAI = true
return lines.join('\n') + '\n';
return lines.join('\n') + '\n';
}
}
+// The endings that make a path a file rather than a page (4.108.1). Any dot used to count, so a page named after a
+// domain (Help's /sites/openvibe.blog, Host's /catalog/domains/co.uk) lost its twin, unless its ending was longer
+// than eight letters (/sites/openvibe.community kept it).
+const FILE_EXT = new Set(('txt md json jsonl xml rss atom csv tsv yaml yml toml ics vcf webmanifest html htm js mjs cjs css map wasm '
+ + 'png jpg jpeg gif webp avif svg ico bmp tif tiff heic pdf zip gz tgz tar bz2 xz 7z rar dmg exe msi deb rpm apk '
+ + 'mp3 mp4 m4a m4v webm ogg oga ogv wav flac mov avi mkv m3u8 ts srt vtt woff woff2 ttf otf eot sh ps1 py').split(' '));
+
/**
/**
* Whether llms.txt's markdown mode links `url` at its Markdown twin: one of the site's own pages. A file (a last path
* Whether llms.txt's markdown mode links `url` at its Markdown twin: one of the site's own pages. A file (a last path
- * segment with an extension: /openapi.json, /feed.xml, /llms-full.txt), an /api/ or /.well-known/ path and, when
+ * segment with a file ending: /openapi.json, /feed.xml, /llms-full.txt), an /api/ or /.well-known/ path and, when
* `base` is given, another origin's link keep their URL (4.108.0; before, /openapi.json became /openapi.json.md).
* `base` is given, another origin's link keep their URL (4.108.0; before, /openapi.json became /openapi.json.md).
*/
*/
function twinnable(url, base) {
function twinnable(url, base) {
@@ -287,7 +294,9 @@ function twinnable(url, base) {
@@ -287,7 +294,9 @@ function twinnable(url, base) {
if (!/^https?:$/.test(u.protocol)) return false;
if (!/^https?:$/.test(u.protocol)) return false;
if (base && /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(String(url)) && u.origin !== new URL(base).origin) return false;
if (base && /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(String(url)) && u.origin !== new URL(base).origin) return false;
if (/^\/(api|\.well-known)(\/|$)/.test(u.pathname)) return false;
if (/^\/(api|\.well-known)(\/|$)/.test(u.pathname)) return false;
- return !/\.[a-z0-9]{1,8}$/i.test(u.pathname.split('/').pop());
+ const last = u.pathname.split('/').pop();
+ const dot = last.lastIndexOf('.');
+ return dot < 0 || !FILE_EXT.has(last.slice(dot + 1).toLowerCase());
}
}
/**
/**
@@ -36,4 +36,11 @@ const old = seo.llmsTxt({ name: 'X', markdown: true, sections: [{ title: 'P', li
@@ -36,4 +36,11 @@ const old = seo.llmsTxt({ name: 'X', markdown: true, sections: [{ title: 'P', li
assert.deepStrictEqual(links(old), ['https://example.com/index.md', 'https://example.com/openapi.json']);
assert.deepStrictEqual(links(old), ['https://example.com/index.md', 'https://example.com/openapi.json']);
// Without markdown nothing changes.
// Without markdown nothing changes.
assert.deepStrictEqual(links(seo.llmsTxt({ name: 'X', sections: [{ title: 'P', links: [{ title: 'Hot', url: '/hot' }] }] })), ['/hot']);
assert.deepStrictEqual(links(seo.llmsTxt({ name: 'X', sections: [{ title: 'P', links: [{ title: 'Hot', url: '/hot' }] }] })), ['/hot']);
+// 4.108.1: a page named after a domain is a page; only a real file ending makes a file.
+const domains = seo.llmsTxt({ name: 'Help', markdown: true, base: 'https://openvibe.help', sections: [{ title: 'Sites', links: [
+ { title: 'Blog', url: 'https://openvibe.help/sites/openvibe.blog' }, { title: 'Community', url: 'https://openvibe.help/sites/openvibe.community' },
+ { title: 'UK', url: '/catalog/domains/co.uk' }, { title: 'Data', url: '/export.CSV' }, { title: 'Old page', url: '/legacy/index.html' }, { title: 'v2.1', url: '/releases/v2.1' },
+] }] });
+assert.deepStrictEqual(links(domains), ['https://openvibe.help/sites/openvibe.blog.md', 'https://openvibe.help/sites/openvibe.community.md',
+ '/catalog/domains/co.uk.md', '/export.CSV', '/legacy/index.html', '/releases/v2.1.md']);
console.log('llms markdown links: ok');
console.log('llms markdown links: ok');