Skip to content

KB Site Astro/Starlight Fixes — Implementation Log

Created 2026-02-28
Updated 2026-02-28
Type project-log
Status in-progress
Tags kb-siteastrostarlightimplementation-log

After reverting from Quartz back to Astro/Starlight, three major issues were identified on the live site at kb.baseworks.com:

  1. Broken wikilinks — Internal [wikilinks](/wikilinks/) throughout the vault not resolving to correct Starlight routes, causing extensive 404s
  2. Duplicate sidebar entries — Each PARA section showing duplicate names (e.g., “Projects” / “Projects”)
  3. Missing Obsidian properties — Frontmatter (created, updated, tags, type, status) not displayed on pages
  • Framework: Astro + Starlight (static site generator)
  • Vault: ~/Obsidian/baseworks-kb-shared-brain/
  • Site code: site/ subdirectory within vault
  • Deployment: Cloudflare Pages via GitHub Actions (behind Cloudflare Access)
  • Repo: p-oancia/baseworks-kb-shared-brain
  • backup/pre-kb-fixes-2026-02-28 — State before any fixes began
  • backup/kb-fixes-complete-2026-02-28 — Tagged after the first batch of commits

1. a3a3880 — fix: use hub docs as section index pages to eliminate sidebar duplicates

Section titled “1. a3a3880 — fix: use hub docs as section index pages to eliminate sidebar duplicates”

Problem: Each PARA section had a self-named hub doc (e.g., 02-areas/02-areas.md) that was synced as a regular page AND a generated index.md, causing duplicate sidebar entries.

Fix in site/scripts/sync-content.mjs:

  • Detect hub docs matching pattern XX-sectionname/XX-sectionname.md
  • Skip them during regular sync via skipFiles parameter
  • Use hub doc content AS the section index.md instead of generating a bland link list
Section titled “2. 1fb7fb7 — feat: add page index generation for wikilink resolution”

Problem: The wikilink remark plugin had no way to know which pages exist or what their Starlight routes are.

Fix in site/scripts/sync-content.mjs:

  • Added buildPageIndex() function that walks all synced pages
  • Added walkForIndex() recursive walker
  • Generates src/content/page-index.json mapping every possible wikilink target to its Starlight route
  • Index keys include: full site path, vault path (with number prefixes), within-section path, and filename-only
  • Initial build: ~1,951 entries
  • Added page-index.json and synced content dirs to .gitignore
Section titled “3. 38b03f2 — feat: rewrite wikilink plugin with page-index-based resolution”

Problem: Plugin was naively lowercasing and hyphenating — no vault-to-Starlight path mapping.

Full rewrite of site/plugins/remark-obsidian-wikilinks.mjs:

  • Loads page-index.json at plugin initialization
  • Resolution chain:
    1. Exact match on normalized target
    2. Match with vault number-prefixes stripped (02-areasareas)
    3. Filename-only match (shortest path)
    4. Fallback: naive URL construction

4. 16c08c3 — feat: display Obsidian frontmatter properties below page titles

Section titled “4. 16c08c3 — feat: display Obsidian frontmatter properties below page titles”

Problem: Starlight’s schema drops unknown frontmatter fields and doesn’t render custom metadata.

Three-part fix:

A. Schema extension (site/src/content.config.ts):

  • Extended docsSchema() with z.object({...}) for: created, updated, tags, type, status, format, location, program_dates, sessions, enrollment_cap, price, segments, lessons, languages, lesson_types
  • Used z.union([z.number(), z.string()]) for fields that could be either (e.g., enrollment_cap: “TBD”)
  • Used z.array(z.coerce.string()) for tags (some YAML tags like 2026 parse as numbers)

B. Component override (site/src/components/PageTitle.astro) — NEW file:

  • Overrides Starlight’s PageTitle component
  • Renders properties in a styled grid below the <h1>
  • Shows tags as styled chips
  • Inlined PAGE_TITLE_ID = '_top' since @astrojs/starlight/constants is not exported

C. Config registration (site/astro.config.mjs):

  • Registered PageTitle in Starlight’s component overrides

D. Styles (site/src/styles/custom.css):

  • .obs-properties — subtle bordered container
  • .obs-properties-grid — responsive grid for key-value pairs
  • .obs-property-key / .obs-property-value — styled labels
  • .obs-tags / .obs-tag — accent-colored tag chips
Section titled “5. e0c7274 — fix: eliminate duplicate h1, sidebar labels, and 404 folder links”

Three sub-fixes:

A. Duplicate h1 headings:

  • Added stripMatchingH1() to sync script
  • When frontmatter title matches the first # Heading in the body, strips the body heading
  • Prevents Starlight rendering both PageTitle component and body h1

B. Sidebar duplicate labels:

  • Added injectSidebarLabel() — sets sidebar: {label: 'Overview'} in frontmatter on hub doc index pages
  • Prevents the index page from showing the same label as the section group

C. Auto-generate missing index pages:

  • Added generateMissingIndexes() — recursively walks all subdirectories
  • Generates index.md with a title and link list for any directory missing one
  • Created 69 index pages (fixes 404s for folder references like /areas/primer/transcripts-en/)

D. Wikilink plugin improvements:

  • Strip trailing slashes from targets before lookup
  • Handle escaped pipes \| in table wikilinks (Obsidian escapes | as \| in markdown tables)
  • Register directory names as page-index keys for index pages

Filename/directory normalization for slug consistency

Section titled “Filename/directory normalization for slug consistency”

Problem discovered: Files with dots in names (e.g., 01.01-the-problem.md) were being copied with their original names, but Starlight slugifies them differently (strips dots → 0101-the-problem). Similarly, directory names with spaces and parentheses (e.g., 2026 (Winter) Study Group Montreal) caused path mismatches.

Changes in site/scripts/sync-content.mjs (uncommitted):

  • Added slugifyName(name, isFile) function — normalizes filenames/dirnames during copy
  • Added normalizeSlug(str) function — core slug normalization:
    • Lowercase
    • Remove parentheses ()
    • Remove dots .
    • Replace spaces with hyphens
    • Replace other special chars with hyphens
    • Collapse multiple hyphens
    • Trim leading/trailing hyphens
  • Modified syncDir() to normalize directory and file names during sync copy

Changes in site/plugins/remark-obsidian-wikilinks.mjs (uncommitted):

  • Updated resolveWikilink() normalization to apply same slug transformation per-segment
  • Each path segment goes through: lowercase → strip parens → strip dots → spaces-to-hyphens → special-chars-to-hyphens → collapse-hyphens

Effect: Reduced broken link count from 489 → 355 instances (264 → 221 targets).

Section titled “6. e7bdad8 — fix: resolve broken wikilinks with slug normalization and pre-parse conversion”

Three key fixes that reduced broken links from 264 targets to 32:

A. Expanded page index sub-paths:

  • walkForIndex now registers ALL suffix sub-paths (not just within-section)
  • For areas/primer/summaries-en/0101-the-problem, also registers summaries-en/0101-the-problem
  • Page index grew from ~2200 to 3070 entries

B. Pre-parse wikilink conversion (major fix):

  • After building page index, a second pass converts all [wikilinks](/wikilinks/) to standard [text](url) markdown
  • This runs BEFORE remark parses the files, avoiding AST splitting issues
  • Critical insight: remark’s inline parser splits [...](//) across multiple AST nodes (especially in GFM tables), preventing the remark plugin from seeing them as wikilinks
  • 147 files had wikilinks converted

C. Underscore prefix stripping:

  • Files starting with _ (like _session-summary-guidelines.md) are renamed during sync (Astro skips _ prefixed files)
  • Added normalizeSlug strip of leading underscores

D. Added audit-links.mjs — automated broken link audit script

Remaining 32 broken targets are all genuine dead links to pages that don’t exist in the vault (e.g., montreal-film-schools-master-list, brand-guidelines, terminology). These cannot be fixed by the build system.


Previously, walkForIndex only registered within-section paths (stripped top-level section like areas/). This left intermediate sub-paths unindexed. Now ALL suffix sub-paths are registered. For areas/primer/summaries-en/0101-the-problem:

  • areas/primer/summaries-en/0101-the-problem
  • primer/summaries-en/0101-the-problem
  • summaries-en/0101-the-problem ✅ (previously missing)
  • 0101-the-problem
  • 0101-the-problem ✅ (but ambiguous — both summaries-en and transcripts-en have this)

Missing key:

  • summaries-en/0101-the-problem ❌ — This is how Obsidian links reference it!

walkForIndex needs to register ALL possible suffix sub-paths for each page. For areas/primer/summaries-en/0101-the-problem, that means also registering:

  • primer/summaries-en/0101-the-problem (already done — within-section)
  • summaries-en/0101-the-problem (MISSING)

The resolver could also be enhanced to try progressively shorter path suffixes.


FileStatusDescription
site/scripts/sync-content.mjsModified (236→480 lines)Hub doc handling, page index, H1 stripping, sidebar labels, auto indexes, slug normalization
site/plugins/remark-obsidian-wikilinks.mjsRewritten (170 lines)Page-index-based resolution with multi-strategy lookup
site/src/content.config.tsModifiedExtended schema for 15 custom frontmatter fields
site/src/components/PageTitle.astroNEWComponent override for properties display
site/src/styles/custom.cssModifiedAdded .obs-properties, .obs-tags CSS
site/astro.config.mjsModifiedRegistered PageTitle component override
site/.gitignoreModifiedIgnore generated docs dirs and page-index.json
  1. node site/scripts/sync-content.mjs — copies vault markdown into site/src/content/docs/
  2. During copy: normalizes filenames, ensures frontmatter, strips duplicate H1s, generates indexes
  3. After copy: builds page-index.json manifest
  4. astro build / astro dev — reads content, remark plugin resolves wikilinks using page-index

Slug normalization function (shared between sync script and remark plugin)

Section titled “Slug normalization function (shared between sync script and remark plugin)”
function normalizeSlug(str) {
return str
.toLowerCase()
.replace(/[()]/g, '') // remove parentheses
.replace(/\./g, '') // remove dots (01.01 → 0101)
.replace(/\s+/g, '-') // spaces → hyphens
.replace(/[^a-z0-9_-]/g, '-') // other special chars → hyphens
.replace(/-{2,}/g, '-') // collapse multiple hyphens
.replace(/^-|-$/g, ''); // trim leading/trailing hyphens
}
  1. Normalize target with same slug function
  2. Direct lookup in page-index
  3. Strip vault number prefixes (02-areasareas) and retry
  4. Filename-only lookup (last path segment)
  5. Fallback: construct URL from normalized path
  • Before any fixes: Unknown (all wikilinks were broken)
  • After page index + plugin rewrite: 489 broken instances, 264 unique targets
  • After filename normalization: 355 broken instances, 221 unique targets
  • After sub-path fix + pre-parse conversion: 32 unique targets (all genuine dead links)
  1. Deploy to production
  2. Optionally create stub pages for the 32 dead link targets
  3. Address any visual/styling issues
Terminal window
# Sync content and rebuild
node site/scripts/sync-content.mjs
# Dev server
cd site && npm run dev
# Production build
cd site && npm run build
# Run broken link audit (build output grep)
cd site && npm run build 2>&1 | grep -c "404"