Incognito-Wiki/docs/superpowers/plans/2026-08-15-programme-sidebar-and-course-integration.md
msa46 1d4d832b6c
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
fix: verify programme routes across deployment bases
2026-08-15 21:09:25 +02:00

24 KiB
Raw Blame History

Programme Sidebar and Course Integration Implementation Plan

Execution note: Follow this plan task by task with test-driven development. Keep main green at every commit and do not merge origin/isaacs-changes; copy only the five reviewed substantive pages from commit d5d6730.

Goal: Publish the useful Computer Science material, give Computer Science and Data Science & AI stable programme-specific routes, remove duplicated shared course bodies, and show a route-aware programme switch without hiding global wiki navigation.

Architecture: The docs collection owns public pages. Seven shared course bodies live as unpublished MDX partials and are imported by thin programme-specific wrappers. Pure navigation helpers select the programme, paired switch target, and sidebar tree from the pathname; Astro middleware applies that tree to Starlight route data, while a Sidebar component override renders the accessible switch and delegates the actual navigation to Starlight's default component. Astro redirects preserve every legacy /bachelor/... route.

Stack: Astro 7, Starlight 0.41, MD/MDX content collections, Astro middleware and component overrides, Node's built-in test runner, linkedom, existing migration/audit scripts.


Non-negotiable content decisions

  • Treat all current src/content/docs/bachelor/** pages as Data Science & AI and move them to src/content/docs/data-science-and-ai/**.
  • 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-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:
Shared body Data Science & AI route Computer Science route
Discrete Mathematics year-1/block-1/discrete-mathematics year-1/period-1/discrete-mathematics
Procedural Programming year-1/block-1/procedural-programming year-1/period-1/procedural-programming
Calculus year-1/block-2/calculus year-1/period-2/calculus
Logic year-1/block-2/logic year-1/period-2/logic
Objects in Programming year-1/block-2/objects-in-programming year-1/period-2/objects-in-programming
Data Structures and Algorithms year-1/block-4/data-structures-and-algorithms year-1/period-4/data-structures-and-algorithms
Linear Algebra year-1/block-4/linear-algebra year-1/period-4/linear-algebra

Task 1: Add migration-contract tests for the canonical DSAI route move

Files:

  • Create: tests/programme-content.test.mjs
  • Modify: tests/content-audit.test.mjs
  • Modify: tests/project-structure.test.mjs
  • Modify: tests/pdf-archive.test.mjs
  • Modify: scripts/audit-content.mjs
  • Modify: scripts/lib/migration.mjs
  • Modify: docs/migration-manifest.json
  • Modify: docs/migration-report.md
  • Modify: docs/supplemental-content.json
  • Modify: src/content/docs/index.mdx
  • Rename: src/content/docs/bachelor/ to src/content/docs/data-science-and-ai/

Step 1: Write the failing route/provenance assertions

In tests/programme-content.test.mjs, add assertions that:

const legacyRoot = 'src/content/docs/bachelor';
const dsaiRoot = 'src/content/docs/data-science-and-ai';

await assert.rejects(stat(legacyRoot), { code: 'ENOENT' });
assert.ok((await stat(dsaiRoot)).isDirectory());

Also recursively inspect the manifest, supplemental registry, migration report, landing page, audit exceptions, and migration canonical map so no destination or internal link contains src/content/docs/bachelor/ or href="./bachelor/", while the historical DokuWiki source identifiers under pages/study/bachelor remain unchanged.

Update existing tests to expect data-science-and-ai destinations and landing-page links. Keep the expected source count 29, unique migration destinations 28, and migration-report row count 29; only destination paths change in this task.

Step 2: Run the focused tests and confirm red

Run:

node --test tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs

Expected: failure because the DSAI tree and provenance records still use bachelor.

Step 3: Move the tree and update provenance atomically

Rename the directory once so Git can retain rename history:

mv src/content/docs/bachelor src/content/docs/data-science-and-ai

Update all destination paths in the manifest, migration report, supplemental registry, audit exception map, and canonical source map from src/content/docs/bachelor/ to src/content/docs/data-science-and-ai/. Update internal public links from /bachelor/ to /data-science-and-ai/, including the landing-page card. Do not change the original source paths or DokuWiki IDs because they document provenance.

Update the programme overview/year wording from generic “Bachelor” to “Data Science & AI” only where the page is describing the current published destination; preserve quoted or historical source terminology when necessary for accuracy.

Step 4: Run the focused tests and content audit

Run:

node --test tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs
npm run audit:content

Expected: all pass, with the same 29 sources, 28 unique manifest destinations, 86 published pages, and 58 supplemental pages.

Step 5: Commit

git add src/content/docs/data-science-and-ai src/content/docs/index.mdx docs/migration-manifest.json docs/migration-report.md docs/supplemental-content.json scripts/audit-content.mjs scripts/lib/migration.mjs tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs
git commit -m "refactor: give data science canonical programme routes"

Task 2: Preserve every legacy bachelor URL with exact redirects

Files:

  • Create: src/config/legacy-bachelor-redirects.mjs
  • Create: tests/programme-redirects.test.mjs
  • Modify: astro.config.mjs

Step 1: Write a failing redirect coverage test

Build expected redirects by recursively listing all .md and .mdx pages under src/content/docs/data-science-and-ai. Convert index files to directory routes and all other filenames to extensionless trailing-slash routes. Assert that legacyBachelorRedirects has exactly one key for every DSAI page after replacing the /data-science-and-ai/ prefix with /bachelor/, and that every value is the exact canonical DSAI route.

Add explicit examples:

assert.equal(legacyBachelorRedirects['/bachelor/'], '/data-science-and-ai/');
assert.equal(
  legacyBachelorRedirects['/bachelor/year-1/block-2/calculus/'],
  '/data-science-and-ai/year-1/block-2/calculus/',
);
assert.equal(
  legacyBachelorRedirects['/bachelor/year-3/study-abroad/'],
  '/data-science-and-ai/year-3/study-abroad/',
);

Also assert astro.config.mjs passes the map through the top-level redirects property.

Step 2: Run the test and confirm red

node --test tests/programme-redirects.test.mjs

Expected: module-not-found or missing redirects.

Step 3: Implement the explicit redirect map

Create legacy-bachelor-redirects.mjs as a checked-in plain object. Do not derive redirects at runtime from the filesystem. Include overview, all three year indexes, projects, honours, thesis, study abroad, and every currently published DSAI course page. Export a frozen object:

export const legacyBachelorRedirects = Object.freeze({
  '/bachelor/': '/data-science-and-ai/',
  // every exact published descendant follows
});

Import it into astro.config.mjs and set:

redirects: legacyBachelorRedirects,

Step 4: Prove generated redirects work at root and under BASE

Run:

node --test tests/programme-redirects.test.mjs
npm run build
BASE=/wiki npm run build

Inspect representative generated redirect HTML and assert its destination includes the configured base exactly once.

Step 5: Commit

git add astro.config.mjs src/config/legacy-bachelor-redirects.mjs tests/programme-redirects.test.mjs
git commit -m "feat: redirect legacy bachelor routes to data science"

Task 3: Replace seven duplicated course bodies with shared MDX partials

Files:

  • Create: src/content/shared-courses/year-1/*.mdx (7 files)
  • Rename: the 7 DSAI shared pages from .md to .mdx
  • Create: the 7 corresponding src/content/docs/computer-science/year-1/period-*/...mdx wrappers
  • Create: tests/shared-programme-courses.test.mjs
  • Modify: astro.config.mjs
  • Modify: docs/supplemental-content.json

Step 1: Write failing shared-source tests

Define the seven route pairs in the test. For each pair, assert:

  • both published wrappers exist and contain valid title and description frontmatter;
  • both import the same file under src/content/shared-courses/year-1/;
  • neither wrapper contains the course sections duplicated from the partial;
  • the partial is outside the docs collection and contains the substantive body;
  • each wrapper contains the standard historical caution;
  • no shared course appears as a second body file anywhere else.

Also assert Starlight config includes:

markdown: {
  processedDirs: ['./src/content/shared-courses/'],
},

Add the seven Computer Science wrappers to the supplemental registry with a provenance kind such as shared-course-wrapper and retain the DSAI registry entries with their migrated destinations.

Step 2: Run the test and confirm red

node --test tests/shared-programme-courses.test.mjs

Expected: missing partials and Computer Science wrappers.

Step 3: Extract each DSAI body exactly once

For each shared course, retain the existing frontmatter values and historical caution in its DSAI wrapper. Move everything after the caution into the matching partial. The wrapper structure must be:

---
title: Course title
description: Existing verified description
---

import SharedCourse from '../../../../shared-courses/year-1/course-slug.mdx';

:::caution[Historical information]
This information originated in the previous wiki and may be outdated.
:::

<SharedCourse />

Create the Computer Science wrapper with the same shared import and warning, but programme-specific frontmatter. Add an empty, documented programme-notes seam only when content actually diverges later; do not add speculative notes now.

Step 4: Verify preservation and rendering

Run:

node --test tests/shared-programme-courses.test.mjs tests/content-audit.test.mjs
npm run audit:content
npm run build

Expected: both routes render the same substantive headings and links, exactly one <h1> per page, and the shared partials do not become public routes.

Update the audit count assertions to the actual new published-page total: 86 + 7 = 93; supplemental total becomes 58 + 7 = 65. The seven partials are excluded from both published counts.

Step 5: Commit

git add astro.config.mjs src/content/shared-courses src/content/docs/data-science-and-ai/year-1 src/content/docs/computer-science docs/supplemental-content.json tests/shared-programme-courses.test.mjs tests/content-audit.test.mjs
git commit -m "refactor: share common bachelor course content"

Task 4: Import the five substantive Computer Science pages and clean overviews

Files:

  • Create: src/content/docs/computer-science/index.md
  • Create: src/content/docs/computer-science/year-1/index.md
  • Create: five reviewed Computer Science course pages under year-1/period-*
  • Modify: docs/supplemental-content.json
  • Modify: tests/programme-content.test.mjs
  • Modify: tests/content-audit.test.mjs

Step 1: Write failing content-selection tests

Assert the exact five unique course destinations exist, have valid frontmatter, contain the standard historical caution, and contain substantive headings or text from d5d6730.

Assert forbidden imports are absent:

assert.doesNotMatch(allComputerScienceContent, /^#Hello$/m);
assert.doesNotMatch(allComputerScienceContent, /Empty Page/);
assert.doesNotMatch(allComputerScienceContent, /course-description/i);

Assert there are exactly 14 Computer Science published pages at this point: overview, Year 1 overview, five unique courses, and seven shared wrappers. Assert previous-exams-and-documents.md still exists.

Step 2: Run the test and confirm red

node --test tests/programme-content.test.mjs

Expected: five unique pages and two overview pages are missing.

Step 3: Copy only reviewed branch blobs through explicit patches

Use read-only git show d5d6730:<path> to inspect each source, normalize frontmatter and the standard warning, remove any body H1, and add only the five approved pages. Do not perform a merge or checkout from the branch.

Write a concise Computer Science overview and Year 1 overview based only on the published course set. State that the material is recovered historical wiki content and may not represent the current curriculum. Link to official Maastricht University curriculum information and to Previous Exams without inventing year 2 or year 3 content.

Register all seven new pages (five courses plus two overviews) in docs/supplemental-content.json with source commit d5d6730, original branch path where applicable, and a note that malformed/placeholder pages were excluded.

Step 4: Audit and build

node --test tests/programme-content.test.mjs tests/content-audit.test.mjs
npm run audit:content
npm run build

Expected final published count for this content phase: 100 pages, with 72 supplemental destinations (65 + 7). Verify actual counts from the audit and encode them in the tests only after confirming the registry matches the filesystem.

Step 5: Commit

git add src/content/docs/computer-science docs/supplemental-content.json tests/programme-content.test.mjs tests/content-audit.test.mjs
git commit -m "feat: publish reviewed computer science courses"

Task 5: Build pure programme navigation data and route selection

Files:

  • Create: src/config/programme-navigation.mjs
  • Create: tests/programme-navigation.test.mjs
  • Modify: src/config/recovered-course-sidebar.mjs
  • Modify: src/config/sidebar.mjs

Step 1: Write failing pure-function tests

Test programmeForPathname(), programmeSwitchTargets(), and sidebarForPathname() without Astro. Cover:

programmeForPathname('/computer-science/year-1/period-2/calculus/') === 'computer-science'
programmeForPathname('/data-science-and-ai/year-3/') === 'data-science-and-ai'
programmeForPathname('/bachelor/year-1/') === 'data-science-and-ai'
programmeForPathname('/useful-information/') === null

Switch mapping cases:

  • shared CS course -> paired DSAI course;
  • shared DSAI course -> paired CS course;
  • unique CS course -> DSAI Year 1;
  • unique DSAI Year 2/3 course -> Computer Science overview;
  • programme overview/year page -> corresponding overview/year fallback where it exists;
  • global page -> both programme overviews with neither active.

Sidebar assertions must prove:

  • exactly one detailed undergraduate programme tree is visible on programme pages;
  • global pages show compact links to both programme overviews;
  • Home, About, Previous exams, Master AI, Master DSDM, and Useful Information remain present for every route;
  • Useful Information retains all guide children.

Step 2: Run and confirm red

node --test tests/programme-navigation.test.mjs

Expected: navigation module is missing.

Step 3: Implement immutable navigation structures

Export:

export const sharedCoursePairs = Object.freeze({ /* 7 bidirectional route pairs */ });
export const computerScienceSidebar = [/* overview, Year 1, periods and 12 courses */];
export const dataScienceSidebar = [/* migrated overview, Years 13 and all existing pages */];
export const globalSidebar = [/* Home, About, exams, masters, Useful Information */];
export function programmeForPathname(pathname) { /* prefix match incl. /bachelor */ }
export function programmeSwitchTargets(pathname) { /* pair/fallback policy */ }
export function sidebarForPathname(pathname) { /* compose programme/global tree */ }

Normalize the recovered-course keys/slugs from bachelor/... to data-science-and-ai/.... Keep course ordering by period/block, not alphabetically across the whole year. The Computer Science tree contains only Year 1 and only the twelve published courses.

Make src/config/sidebar.mjs export a default global/compact sidebar for Starlight's initial configuration and re-export the selector data needed by middleware. Avoid duplicating masters or Useful Information in two source files.

Step 4: Run tests

node --test tests/programme-navigation.test.mjs tests/useful-guides.test.mjs tests/pdf-archive.test.mjs

Expected: all pass.

Step 5: Commit

git add src/config/programme-navigation.mjs src/config/recovered-course-sidebar.mjs src/config/sidebar.mjs tests/programme-navigation.test.mjs tests/useful-guides.test.mjs tests/pdf-archive.test.mjs
git commit -m "feat: add route-aware programme navigation data"

Task 6: Apply route-aware sidebar data through Astro middleware

Files:

  • Create: src/middleware.ts
  • Create: tests/programme-middleware.test.mjs

Step 1: Write a failing middleware behavior test

Export a small handler factory or pure mutation helper so the Node test can provide a fake context with url.pathname and locals.starlightRoute.sidebar. Assert that it replaces only the sidebar and preserves the rest of route data.

Test Computer Science, DSAI, legacy /bachelor, master, Useful Information, and home paths. Include a no-Starlight-locals guard so non-Starlight routes/assets are unaffected.

Step 2: Run and confirm red

node --test tests/programme-middleware.test.mjs

Expected: middleware module is missing.

Step 3: Implement middleware using Starlight's supported route data

Use Astro's defineMiddleware and the pure selector:

import { defineMiddleware } from 'astro:middleware';
import { sidebarForPathname } from './config/programme-navigation.mjs';

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next();
  if (context.locals.starlightRoute) {
    context.locals.starlightRoute.sidebar = sidebarForPathname(context.url.pathname);
  }
  return response;
});

Adjust ordering to the Starlight 0.41 route-data lifecycle if the focused integration build shows route data must be mutated before next(). Keep all selection logic in the pure module, not in middleware.

Step 4: Run test and focused builds

node --test tests/programme-middleware.test.mjs tests/programme-navigation.test.mjs
npm run build
BASE=/wiki npm run build

Inspect representative built HTML to confirm the active programme tree is present and the inactive detailed tree is absent.

Step 5: Commit

git add src/middleware.ts tests/programme-middleware.test.mjs
git commit -m "feat: select programme sidebar per route"

Task 7: Add the accessible programme switch through a Sidebar override

Files:

  • Create: src/components/ProgrammeSwitch.astro
  • Create: src/components/Sidebar.astro
  • Create: tests/programme-switch.test.mjs
  • Modify: astro.config.mjs
  • Modify: src/styles/incognito.css

Step 1: Write failing structure/accessibility tests

Assert the override is registered as:

components: {
  Sidebar: './src/components/Sidebar.astro',
},

Assert Sidebar.astro imports and renders the custom switch followed by Starlight's default Sidebar component. Assert the switch:

  • uses a <nav aria-label="Bachelor programme"> landmark;
  • renders ordinary <a> links for both programmes;
  • applies aria-current="page" only to the active programme;
  • consumes the pure switch-target helper;
  • has no client directive and no inline script.

Add rendered-output checks using linkedom against representative built pages for desktop/mobile-shared markup and base-prefixed hrefs.

Step 2: Run and confirm red

node --test tests/programme-switch.test.mjs

Expected: missing override and registration.

Step 3: Implement the wrapper, not a copy of Starlight internals

src/components/Sidebar.astro should remain minimal:

---
import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
import ProgrammeSwitch from './ProgrammeSwitch.astro';
---

<ProgrammeSwitch />
<DefaultSidebar />

ProgrammeSwitch.astro reads Astro.url.pathname, obtains base-aware targets from the pure helper, and renders two compact links. Use Astro's base URL support so root and /wiki/ builds do not hard-code /.

Style the control in incognito.css using existing Starlight color tokens. Keep a visible focus state, do not rely on color alone for active state, permit wrapping at narrow widths, and keep link targets at least as tall as surrounding sidebar links.

Step 4: Verify component and output

node --test tests/programme-switch.test.mjs
npm run build
npm run check:rendered
npm run check:links
BASE=/wiki npm run build
npm run check:rendered
BASE=/wiki npm run check:links

Expected: accessible switch appears on all Starlight pages, correct option is active on programme pages, neither is active on global pages, and all links honor BASE.

Step 5: Commit

git add astro.config.mjs src/components/ProgrammeSwitch.astro src/components/Sidebar.astro src/styles/incognito.css tests/programme-switch.test.mjs
git commit -m "feat: add accessible bachelor programme switch"

Task 8: Final integration audit, documentation reconciliation, and push

Files:

  • Modify as needed: README.md
  • Modify as needed: docs/migration-report.md
  • Modify: tests containing obsolete counts/routes

Step 1: Search for accidental legacy or rejected content

Run:

rg -n "src/content/docs/bachelor|href=\"(?:\./|/)bachelor|slug: 'bachelor|#Hello|Empty Page" src docs tests scripts astro.config.mjs

Expected: no old destination/sidebar/link references and no rejected branch content. Historical DokuWiki source IDs and the explicit redirect keys are allowed and should be reviewed individually.

Step 2: Run the complete verification suite at the root base

npm run verify

Expected: Astro check, all Node tests, content audit, build, H1/unpublished-output audit, and internal-link audit all pass.

Step 3: Repeat build/output verification with a non-root base

BASE=/wiki npm run build
npm run check:rendered
BASE=/wiki npm run check:links

Expected: all programme switch links, content links, Matomo assets, and redirect targets contain /wiki/ exactly once where required.

Step 4: Review the diff against the approved exclusions

Run:

git diff --check
git status --short
git diff --stat origin/main...HEAD
git log --oneline origin/main..HEAD

Confirm:

  • previous-exams-and-documents.md remains present;
  • no empty year 2/year 3 Computer Science files exist;
  • no malformed duplicate description exists;
  • seven shared bodies have one source each;
  • DSAI retained all prior pages under the new canonical prefix;
  • every old bachelor route redirects;
  • global sidebar groups remain visible.

Step 5: Commit any final reconciliation

If final verification required documentation or assertion corrections:

git add README.md docs tests
git commit -m "docs: reconcile programme migration records"

Do not create an empty commit.

Step 6: Push main and verify the Forgejo remote

git push origin main
git ls-remote --heads origin main
git rev-parse HEAD

Expected: the remote main hash exactly equals local HEAD. Report the final commit range, verification commands, published course selection, redirect coverage count, and why origin/isaacs-changes was not merged wholesale.