KB Site Astro/Starlight Fixes — Implementation Log
Overview
Section titled “Overview”After reverting from Quartz back to Astro/Starlight, three major issues were identified on the live site at kb.baseworks.com:
- Broken wikilinks — Internal
[wikilinks](/wikilinks/)throughout the vault not resolving to correct Starlight routes, causing extensive 404s - Duplicate sidebar entries — Each PARA section showing duplicate names (e.g., “Projects” / “Projects”)
- 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 Tags
Section titled “Backup Tags”backup/pre-kb-fixes-2026-02-28— State before any fixes beganbackup/kb-fixes-complete-2026-02-28— Tagged after the first batch of commits
Commits (chronological)
Section titled “Commits (chronological)”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
skipFilesparameter - Use hub doc content AS the section
index.mdinstead of generating a bland link list
2. 1fb7fb7 — feat: add page index generation for wikilink resolution
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.jsonmapping 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.jsonand synced content dirs to.gitignore
3. 38b03f2 — feat: rewrite wikilink plugin with page-index-based resolution
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.jsonat plugin initialization - Resolution chain:
- Exact match on normalized target
- Match with vault number-prefixes stripped (
02-areas→areas) - Filename-only match (shortest path)
- 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()withz.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 like2026parse as numbers)
B. Component override (site/src/components/PageTitle.astro) — NEW file:
- Overrides Starlight’s
PageTitlecomponent - Renders properties in a styled grid below the
<h1> - Shows tags as styled chips
- Inlined
PAGE_TITLE_ID = '_top'since@astrojs/starlight/constantsis not exported
C. Config registration (site/astro.config.mjs):
- Registered
PageTitlein 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
5. e0c7274 — fix: eliminate duplicate h1, sidebar labels, and 404 folder links
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
titlematches the first# Headingin the body, strips the body heading - Prevents Starlight rendering both PageTitle component and body h1
B. Sidebar duplicate labels:
- Added
injectSidebarLabel()— setssidebar: {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.mdwith 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
Uncommitted Work (in progress)
Section titled “Uncommitted Work (in progress)”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).
6. e7bdad8 — fix: resolve broken wikilinks with slug normalization and pre-parse conversion
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:
walkForIndexnow registers ALL suffix sub-paths (not just within-section)- For
areas/primer/summaries-en/0101-the-problem, also registerssummaries-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
normalizeSlugstrip 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.
Resolved: Page Index Sub-path Gap
Section titled “Resolved: Page Index Sub-path Gap”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!
Fix needed
Section titled “Fix needed”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.
All Modified Files Summary
Section titled “All Modified Files Summary”| File | Status | Description |
|---|---|---|
site/scripts/sync-content.mjs | Modified (236→480 lines) | Hub doc handling, page index, H1 stripping, sidebar labels, auto indexes, slug normalization |
site/plugins/remark-obsidian-wikilinks.mjs | Rewritten (170 lines) | Page-index-based resolution with multi-strategy lookup |
site/src/content.config.ts | Modified | Extended schema for 15 custom frontmatter fields |
site/src/components/PageTitle.astro | NEW | Component override for properties display |
site/src/styles/custom.css | Modified | Added .obs-properties, .obs-tags CSS |
site/astro.config.mjs | Modified | Registered PageTitle component override |
site/.gitignore | Modified | Ignore generated docs dirs and page-index.json |
Key Technical Details
Section titled “Key Technical Details”Content sync pipeline
Section titled “Content sync pipeline”node site/scripts/sync-content.mjs— copies vault markdown intosite/src/content/docs/- During copy: normalizes filenames, ensures frontmatter, strips duplicate H1s, generates indexes
- After copy: builds
page-index.jsonmanifest 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}Wikilink resolution chain
Section titled “Wikilink resolution chain”- Normalize target with same slug function
- Direct lookup in page-index
- Strip vault number prefixes (
02-areas→areas) and retry - Filename-only lookup (last path segment)
- Fallback: construct URL from normalized path
Link audit results over time
Section titled “Link audit results over time”- 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)
Next Steps
Section titled “Next Steps”- Deploy to production
- Optionally create stub pages for the 32 dead link targets
- Address any visual/styling issues
Commands Reference
Section titled “Commands Reference”# Sync content and rebuildnode site/scripts/sync-content.mjs
# Dev servercd site && npm run dev
# Production buildcd site && npm run build
# Run broken link audit (build output grep)cd site && npm run build 2>&1 | grep -c "404"