From afedd16d56e2f07ce1e72a19f2364dc1187f5925 Mon Sep 17 00:00:00 2001 From: carlh171112 Date: Thu, 2 Jul 2026 10:31:12 -0700 Subject: [PATCH] feat(special): add browsable Special:LonelyPages report page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a browsable `/wiki/special/lonelypages/` page for the orphaned-articles report that previously only existed as `lonelypages.json`, mirroring the Special:WantedPages HTML page (#1953). LonelyPages is the direct complement of Special:MostLinkedPages, which already has an HTML page. - render the report from the same shared `buildLonelyPages` builder over the same `public/data/backlinks.json` the JSON endpoint reads, so the HTML page and JSON companion stay in lockstep - register the route in the sitemap and share-preview metadata (the latter drives the `/og/special/lonelypages.png` share card, keeping the bidirectional special-page ↔ OG-card invariant satisfied) - extend check-lonely-pages.js and check-sitemap.js to assert the HTML route Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/check-lonely-pages.js | 36 +++++++++++- scripts/check-sitemap.js | 2 +- src/lib/share-preview.ts | 7 +++ src/pages/sitemap.xml.ts | 1 + src/pages/wiki/special/lonelypages.astro | 75 ++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/pages/wiki/special/lonelypages.astro diff --git a/scripts/check-lonely-pages.js b/scripts/check-lonely-pages.js index f5d0268a..b7759564 100644 --- a/scripts/check-lonely-pages.js +++ b/scripts/check-lonely-pages.js @@ -175,6 +175,40 @@ data.pages.forEach((row, i) => { } }); +// ---- 3) Built HTML route: the browsable Special:LonelyPages page ------------ +// +// The human-readable Special:LonelyPages route is the canonical browsable partner +// to lonelypages.json. Keep the built page aligned with the JSON route: same +// heading, same empty state, and the expected article + backlinks links for each +// orphan, all ordered by the same shared buildLonelyPages ranking. +const distHtml = path.join(wikiDir, 'special', 'lonelypages', 'index.html'); +assert.ok(fs.existsSync(distHtml), 'dist/wiki/special/lonelypages/index.html not found; run the build first'); +const html = fs.readFileSync(distHtml, 'utf8'); + +assert.match( + html, + /]*class="firstHeading"[^>]*>Lonely pages<\/h1>/, + 'lonelypages HTML page must render the Lonely pages heading', +); +if (expected.length === 0) { + assert.match( + html, + /No lonely pages right now\./, + 'lonelypages HTML page must render the empty-state copy when the report is empty', + ); +} else { + for (const entry of expected) { + assert.ok( + html.includes(`href="/wiki/${entry.slug}/"`), + `lonelypages HTML page must link to the orphaned article /wiki/${entry.slug}/`, + ); + assert.ok( + html.includes(`/wiki/${entry.slug}/backlinks/`), + `lonelypages HTML page must link to the orphan's backlinks page /wiki/${entry.slug}/backlinks/`, + ); + } +} + console.log( - `Lonely pages check passed (${data.pages.length} orphaned pages from the built endpoint match the link graph; lonely + most-linked partition all ${Object.keys(realTitleBySlug).length} published articles)`, + `Lonely pages check passed (${data.pages.length} orphaned pages from the built endpoint match the link graph; lonely + most-linked partition all ${Object.keys(realTitleBySlug).length} published articles; HTML route aligned)`, ); diff --git a/scripts/check-sitemap.js b/scripts/check-sitemap.js index a19d8db1..82d4a621 100644 --- a/scripts/check-sitemap.js +++ b/scripts/check-sitemap.js @@ -52,7 +52,7 @@ assert.ok(articleUrls > 0, 'no article URLs found in the sitemap'); // The special content overview pages are canonical, indexable routes and must // stay in the sitemap so discovery does not regress when a new one is added. -for (const special of ['allpages', 'categories', 'mostlinkedpages', 'recentchanges', 'statistics', 'subnets', 'wantedpages']) { +for (const special of ['allpages', 'categories', 'lonelypages', 'mostlinkedpages', 'recentchanges', 'statistics', 'subnets', 'wantedpages']) { assert.ok( xml.includes(`/wiki/special/${special}/`), `sitemap must include the /wiki/special/${special}/ page`, diff --git a/src/lib/share-preview.ts b/src/lib/share-preview.ts index 41b583ad..20cb04c5 100644 --- a/src/lib/share-preview.ts +++ b/src/lib/share-preview.ts @@ -18,6 +18,13 @@ const specialPages: SpecialPageMeta[] = [ description: 'Browse all topics in the Taopedia Bittensor knowledge base.', label: 'Topic directory', }, + { + name: 'lonelypages', + title: 'Lonely pages', + description: + 'Orphaned articles across the Taopedia Bittensor knowledge base — published pages that no other article links to.', + label: 'Orphaned-page report', + }, { name: 'mostlinkedpages', title: 'Most linked pages', diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index b90280a6..76198009 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -73,6 +73,7 @@ export const GET: APIRoute = ({ site }) => { { path: '/', lastmod: '' }, { path: '/wiki/special/allpages/', lastmod: '' }, { path: '/wiki/special/categories/', lastmod: '' }, + { path: '/wiki/special/lonelypages/', lastmod: '' }, { path: '/wiki/special/mostlinkedpages/', lastmod: '' }, { path: '/wiki/special/recentchanges/', lastmod: '' }, { path: '/wiki/special/statistics/', lastmod: '' }, diff --git a/src/pages/wiki/special/lonelypages.astro b/src/pages/wiki/special/lonelypages.astro new file mode 100644 index 00000000..7fbd9107 --- /dev/null +++ b/src/pages/wiki/special/lonelypages.astro @@ -0,0 +1,75 @@ +--- +import WikiLayout from '../../../layouts/WikiLayout.astro'; +import { publishedTitleBySlug } from '../../../lib/article-metadata'; +import { buildLonelyPages } from '../../../../scripts/lonely-pages.js'; + +// Special:LonelyPages — the browsable partner to lonelypages.json: published +// articles that NO other published article links to (zero inbound links), the +// exact complement of Special:MostLinkedPages. Both surfaces derive their +// selection and ordering from the SAME shared builder (scripts/lonely-pages.js) +// over the SAME public/data/backlinks.json the machine-readable endpoint reads, +// so the HTML page and JSON companion stay in lockstep. Loaded via glob so a +// missing link graph (e.g. a dev run before the build) degrades to an empty list. +const titleBySlug = publishedTitleBySlug(); + +const backlinksModules = import.meta.glob('../../../../public/data/backlinks.json', { eager: true }) as Record< + string, + { default?: Record> } +>; +const backlinksData = Object.values(backlinksModules)[0]?.default ?? {}; + +const lonelyPages = buildLonelyPages({ titleBySlug, backlinks: backlinksData }); +--- + + +
+

Lonely pages

+ +
+

+ Orphaned articles that no other published article links to. Wiring these into the link graph + from related pages makes them reachable while browsing, not just from search. +

+ + {lonelyPages.length === 0 ? ( +

No lonely pages right now. Every published article has at least one incoming link from another article.

+ ) : ( +
    + {lonelyPages.map((entry) => ( +
  1. + {entry.title} + No incoming links +
  2. + ))} +
+ )} +
+
+
+ +