Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions scripts/check-lonely-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,18 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
const wikiDir = path.join(projectRoot, 'dist', 'wiki');
const distFile = path.join(wikiDir, 'special', 'lonelypages.json');
const htmlFile = path.join(wikiDir, 'special', 'lonelypages', 'index.html');
const backlinksFile = path.join(projectRoot, 'public', 'data', 'backlinks.json');
const slugmapFile = path.join(projectRoot, 'public', 'data', 'slugmap.json');
const linkgraphFile = path.join(projectRoot, 'public', 'data', 'linkgraph.json');
assert.ok(fs.existsSync(distFile), 'dist/wiki/special/lonelypages.json not found; run the build first');
assert.ok(fs.existsSync(htmlFile), 'dist/wiki/special/lonelypages/index.html not found; run the build first');
assert.ok(fs.existsSync(backlinksFile), 'public/data/backlinks.json not found; run the build first');
assert.ok(fs.existsSync(slugmapFile), 'public/data/slugmap.json not found; run the build first');
assert.ok(fs.existsSync(linkgraphFile), 'public/data/linkgraph.json not found; run the build first');

const data = JSON.parse(fs.readFileSync(distFile, 'utf8'));
const html = fs.readFileSync(htmlFile, 'utf8');
const backlinksData = JSON.parse(fs.readFileSync(backlinksFile, 'utf8'));
const slugmap = JSON.parse(fs.readFileSync(slugmapFile, 'utf8'));
const linkgraphData = JSON.parse(fs.readFileSync(linkgraphFile, 'utf8'));
Expand All @@ -105,6 +108,26 @@ const realTitleBySlug = {};
for (const [slug, entry] of Object.entries(slugmap)) realTitleBySlug[slug] = entry?.title ?? slug;
const expected = buildLonelyPages({ titleBySlug: realTitleBySlug, backlinks: backlinksData });

assert.match(html, /<h1[^>]*>\s*Lonely pages\s*<\/h1>/, 'lonely pages HTML report must render the Lonely pages heading');
const htmlRows = [...html.matchAll(/<tr[^>]*class="mw-lonelypages-row"[^>]*>([\s\S]*?)<\/tr>/g)].map(([, block]) => {
const href = (block.match(/mw-lonelypages-title[^>]*href="([^"]+)"/) || [])[1];
const count = Number((block.match(/<td class="mw-lonelypages-outbound"[^>]*>(\d+)<\/td>/) || [])[1]);
return { href, count };
});
assert.equal(htmlRows.length, expected.length, 'lonely pages HTML report must list every orphaned article exactly once');
htmlRows.forEach((row, i) => {
assert.equal(row.href, `/wiki/${expected[i].slug}/`, `HTML row ${i} must link to the expected orphaned article`);
assert.equal(
row.count,
getArticleReferences({ slug: expected[i].slug, linkGraph: linkgraphData, titleBySlug: realTitleBySlug }).length,
`HTML row ${i} outbound reference count must match the published outbound-reference count`,
);
});
assert.ok(
fs.readFileSync(path.join(projectRoot, 'dist', 'sitemap.xml'), 'utf8').includes('/wiki/special/lonelypages/'),
'sitemap.xml must include /wiki/special/lonelypages/',
);

assert.ok(typeof data.site === 'string' && /^https?:\/\//.test(data.site), `site must be a URL string (got ${JSON.stringify(data.site)})`);
assert.equal(data.lonelypagesJsonUrl, `${data.site}/wiki/special/lonelypages.json`, 'lonelypagesJsonUrl must be the canonical self-link');
assert.ok(Array.isArray(data.pages), 'pages must be an array');
Expand Down
6 changes: 6 additions & 0 deletions src/lib/share-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const specialPages: SpecialPageMeta[] = [
description: 'Browse all topics in the Taopedia Bittensor knowledge base.',
label: 'Topic directory',
},
{
name: 'lonelypages',
title: 'Lonely pages',
description: 'Orphaned Taopedia articles with zero published inbound links, ranked alphabetically.',
label: 'Maintenance report',
},
{
name: 'mostlinkedpages',
title: 'Most linked pages',
Expand Down
1 change: 1 addition & 0 deletions src/pages/sitemap.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' },
Expand Down
107 changes: 107 additions & 0 deletions src/pages/wiki/special/lonelypages.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
import WikiLayout from '../../../layouts/WikiLayout.astro';
import { buildLonelyPages } from '../../../../scripts/lonely-pages.js';
import { getArticleReferences } from '../../../lib/article-references.js';
import { publishedSummaryBySlug, publishedTitleBySlug } from '../../../lib/article-metadata';

const backlinksModules = import.meta.glob('../../../../public/data/backlinks.json', { eager: true }) as Record<
string,
{ default?: Record<string, Array<{ from: string }>> }
>;
const backlinksData = Object.values(backlinksModules)[0]?.default ?? {};
const linkgraphModules = import.meta.glob('../../../../public/data/linkgraph.json', { eager: true }) as Record<
string,
{ default?: Record<string, Array<{ target?: string }>> }
>;
const linkgraphData = Object.values(linkgraphModules)[0]?.default ?? {};

const titleBySlug = publishedTitleBySlug();
const summaryBySlug = publishedSummaryBySlug();
const lonelyPages = buildLonelyPages({ titleBySlug, backlinks: backlinksData }).map((entry) => ({
...entry,
summary: summaryBySlug[entry.slug] || '',
referencesCount: getArticleReferences({ slug: entry.slug, linkGraph: linkgraphData, titleBySlug }).length,
}));
---

<WikiLayout
title="Lonely pages"
description="Orphaned Taopedia articles with zero published inbound links, ranked alphabetically."
>
<div class="mw-body-content">
<h1 class="firstHeading">Lonely pages</h1>

<div class="mw-parser-output">
<p>
Orphaned articles with no published inbound links from other Taopedia pages. The outbound
references column helps distinguish true dead ends from articles that still link out to the
rest of the knowledge base.
</p>

{lonelyPages.length === 0 ? (
<p>No lonely pages found.</p>
) : (
<table class="mw-lonelypages">
<thead>
<tr>
<th scope="col">Article</th>
<th scope="col" class="mw-lonelypages-outbound">Outbound references</th>
</tr>
</thead>
<tbody>
{lonelyPages.map((entry) => (
<tr class="mw-lonelypages-row">
<td>
<a class="mw-lonelypages-title" href={`/wiki/${entry.slug}/`}>{entry.title}</a>
{entry.summary && <div class="mw-lonelypages-summary">{entry.summary}</div>}
</td>
<td class="mw-lonelypages-outbound">{entry.referencesCount}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</WikiLayout>

<style>
.mw-lonelypages {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
}
.mw-lonelypages th,
.mw-lonelypages td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-color-subtle);
text-align: left;
vertical-align: top;
}
.mw-lonelypages th {
color: var(--color-base-subtle);
font-weight: 600;
}
.mw-lonelypages-title {
font-weight: 600;
}
.mw-lonelypages-summary {
margin-top: 6px;
color: var(--color-base-subtle);
font-size: 13px;
}
.mw-lonelypages-outbound {
width: 140px;
white-space: nowrap;
}
@media (max-width: 640px) {
.mw-lonelypages th,
.mw-lonelypages td {
padding: 8px 10px;
}
.mw-lonelypages-outbound {
width: 110px;
}
}
</style>
Loading