fix: verify programme routes across deployment bases
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

This commit is contained in:
msa46 2026-08-15 21:09:25 +02:00
parent 8c471fc0fa
commit 1d4d832b6c
11 changed files with 67 additions and 6 deletions

View file

@ -1,7 +1,9 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import { unified } from '@astrojs/markdown-remark';
import { sidebar } from './src/config/sidebar.mjs';
import { legacyBachelorRedirects } from './src/config/legacy-bachelor-redirects.mjs';
import { baseAwareLinks } from './src/config/base-aware-markdown.mjs';
const site = process.env.SITE || 'http://localhost:4321';
const base = process.env.BASE || '/';
@ -13,7 +15,10 @@ const matomoConsentScript = `${normalizedBase}/matomo-consent.js`;
export default defineConfig({
site,
base,
redirects: legacyBachelorRedirects,
redirects: Object.fromEntries(Object.entries(legacyBachelorRedirects).map(([from, to]) => [from, `${normalizedBase}${to}`])),
markdown: {
processor: unified({ remarkPlugins: [[baseAwareLinks, { base }]] }),
},
integrations: [
starlight({
title: 'Incognito Wiki',

View file

@ -16,9 +16,9 @@
- Preserve every old `/bachelor/...` URL with an explicit permanent redirect to the exact new route.
- Import only these substantive Computer Science pages from `d5d6730`:
- `computer-science/year-1/period-1/introduction-to-computer-science.md`
- `computer-science/year-1/period-2/computer-architecture.md`
- `computer-science/year-1/period-4/algorithmic-design.md`
- `computer-science/year-1/period-4/databases.md`
- `computer-science/year-1/period-4/computer-architecture.md`
- `computer-science/year-1/period-5/algorithmic-design.md`
- `computer-science/year-1/period-5/databases.md`
- `computer-science/year-1/period-5/statistics.md`
- Do not import the malformed `computer-science/course-description.md`, any `Empty Page` placeholder, or the branch deletion of `previous-exams-and-documents.md`.
- Store the body of each byte-identical shared course once, outside `src/content/docs`, and expose it through two programme wrappers:

1
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "incognito-wiki",
"version": "0.1.0",
"dependencies": {
"@astrojs/markdown-remark": "7.2.2",
"@astrojs/starlight": "0.41.6",
"astro": "7.1.6",
"sharp": "0.35.3"

View file

@ -3,7 +3,9 @@
"version": "0.1.0",
"private": true,
"type": "module",
"engines": { "node": ">=22.12.0" },
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
@ -18,6 +20,7 @@
"verify": "npm run check && npm run audit:content && npm run build && npm run check:rendered && npm run check:links"
},
"dependencies": {
"@astrojs/markdown-remark": "7.2.2",
"@astrojs/starlight": "0.41.6",
"astro": "7.1.6",
"sharp": "0.35.3"

View file

@ -90,6 +90,7 @@ export async function checkInternalLinks({ distRoot, base }) {
const pageUrl = new URL(`${normalizedBase.replace(/\/$/, '')}${route}`, 'https://built.invalid');
const html = await readFile(sourcePath, 'utf8');
const { document } = parseHTML(html);
if (document.querySelector('meta[http-equiv="refresh"]')) continue;
for (const element of document.querySelectorAll('[href]')) {
const href = element.getAttribute('href')?.trim() ?? '';

View file

@ -26,6 +26,7 @@ export async function checkRenderedOutput({ distRoot }) {
for (const file of htmlFiles) {
const path = relative(absoluteDistRoot, file).split(sep).join('/');
const { document } = parseHTML(await readFile(file, 'utf8'));
if (document.querySelector('meta[http-equiv="refresh"]')) continue;
const h1Count = document.querySelectorAll('h1').length;
if (h1Count !== 1) headingIssues.push({ file: path, h1Count });
}

View file

@ -3,7 +3,7 @@ import { programmeForPathname, programmeSwitchTargets } from '../config/programm
const active = programmeForPathname(Astro.url.pathname.replace(import.meta.env.BASE_URL.replace(/\/$/, ''), ''));
const targets = programmeSwitchTargets(Astro.url.pathname);
const withBase = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\//, '')}`;
const withBase = (path: string) => `${import.meta.env.BASE_URL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
---
<nav class="programme-switch" aria-label="Bachelor programme">

View file

@ -0,0 +1,19 @@
export function baseAwareLinks({ base = '/' } = {}) {
const prefix = base === '/' ? '' : `/${base.replace(/^\/+|\/+$/g, '')}`;
return function transform(tree) {
if (!prefix) return tree;
const visit = (node) => {
if ((node.type === 'link' || node.type === 'image')
&& typeof node.url === 'string'
&& node.url.startsWith('/')
&& !node.url.startsWith('//')
&& node.url !== prefix
&& !node.url.startsWith(`${prefix}/`)) {
node.url = `${prefix}${node.url}`;
}
if (Array.isArray(node.children)) node.children.forEach(visit);
};
visit(tree);
return tree;
};
}

View file

@ -0,0 +1,15 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { baseAwareLinks } from '../src/config/base-aware-markdown.mjs';
test('prefixes root-relative Markdown links for subpath builds', () => {
const tree = { type: 'root', children: [
{ type: 'link', url: '/previous-exams-and-documents/' },
{ type: 'link', url: 'https://example.com/' },
{ type: 'image', url: '/media/file.pdf' },
] };
baseAwareLinks({ base: '/wiki' })(tree);
assert.deepEqual(tree.children.map(({ url }) => url), [
'/wiki/previous-exams-and-documents/', 'https://example.com/', '/wiki/media/file.pdf',
]);
});

View file

@ -98,6 +98,14 @@ test('ignores fragments, remote protocols, and Astro-generated assets', async ()
});
});
test('ignores links inside generated redirect documents', async () => {
await withBuiltSite(async (root) => {
await writeFile(join(root, 'index.html'), '<meta http-equiv="refresh" content="0;url=/target/"><a href="/target/">Redirect</a>');
const result = await checkInternalLinks({ distRoot: root, base: '/wiki/' });
assert.equal(result.broken.length, 0);
});
});
for (const { name, href } of [
{ name: 'encoded slash', href: '/..%2Fmanual.pdf' },
{ name: 'encoded backslash', href: '/..%5Cmanual.pdf' },

View file

@ -44,3 +44,11 @@ test('reports any to-be-studied path that reaches built output', async () => {
assert.deepEqual(result.unpublishedPaths, ['to-be-studied/leak.txt']);
});
});
test('ignores Astro redirect documents when checking content headings', async () => {
await withRenderedSite(async (root) => {
await writeFile(join(root, 'index.html'), '<meta http-equiv="refresh" content="0;url=/new/"><a href="/new/">Redirect</a>');
const result = await checkRenderedOutput({ distRoot: root });
assert.deepEqual(result.headingIssues, []);
});
});