Front Matter Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add Just The Docs front matter to all content .md files, restructure README/index.md, standardize backlinks, and update config.
Architecture: A reusable Python script handles the bulk front matter addition and backlink fix across ~172 files. Three manual prep steps (create index.md, rewrite README.md, update _config.yml) run first. The script caches git dates in one bulk query, classifies files, builds hierarchy, and applies changes.
Tech Stack: Python 3.10+, Git CLI, Jekyll/JTD front matter (YAML)
Spec: docs/superpowers/specs/2026-03-30-front-matter-design.md
Important: JTD 3-Level Nav Limit
JTD supports max 3 navigation levels. The repo has one depth-3 directory: data/nosql/mongo/. Files there would be nav level 4 (exceeds limit). The script flattens them as level-3 siblings under NoSQL — mongo/index.md becomes a regular page (no has_children), and all mongo leaf files get parent: NoSQL, grand_parent: Data.
Two directories lack index.md: azure/lectures/ and devops/os/. Files there become direct children of the parent directory’s index (Azure, DevOps respectively).
Task 1: Create Root index.md
Files:
-
Create:
index.md -
Step 1: Create root index.md with front matter and content from current README.md
---
title: "Illegitimis' Dev Mnemonics"
layout: home
nav_order: 1
description: "Dev documentation, tutorial, memory pointers, inverse oblivion"
permalink: /
last_modified_date: 2026-03-30 00:00:00 +00:00
---
# Illegitimis' Dev Mnemonics
- [.NET](./dotnet/index.md) — C#, CLR, ASP.NET, data access, parallelism
- [Architecture](./architecture/index.md) — OOP, SOLID, DDD, design patterns
- [Data](./data/index.md) — SQL, NoSQL, MongoDB
- [Web Services](./web-services/index.md) — REST, HTTP, GraphQL, APIs
- [Azure](./azure/index.md) — cloud platform, AZ-900
- [Distributed Systems](./distributed-systems/index.md) — microservices, Docker, messaging
- [Testing](./testing/index.md) — NUnit, xUnit, TDD
- [JavaScript](./javascript/index.md) — JS, Angular, CSS, frontend tooling
- [DevOps](./devops/index.md) — Git, CI/CD, tools
- Step 2: Commit
git add index.md
git commit -m "feat: create root index.md as JTD home page"
Task 2: Rewrite README.md
Files:
-
Modify:
README.md -
Step 1: Replace README.md contents with lean GitHub-facing file
---
nav_exclude: true
search_exclude: true
---
# Dev Mnemonics
A Jekyll-based documentation site covering software development topics —
C#/.NET, architecture, data, web services, Azure, distributed systems,
testing, JavaScript, and DevOps.
## Live Site
[illegitimis.github.io/Tutorial](https://illegitimis.github.io/Tutorial/)
## Local Development
```bash
gem install jekyll bundler
jekyll serve
Contributing
See CONTRIBUTING.md for guidelines.
Resources
-
Wiki — legacy documentation ```
-
Step 2: Commit
git add README.md
git commit -m "feat: rewrite README.md as lean GitHub-facing file"
Task 3: Update _config.yml
Files:
-
Modify:
_config.yml:101 -
Step 1: Change last_edit_time_format
Replace:
last_edit_time_format: "%b %e %Y at %I:%M %p"
With:
last_edit_time_format: "%Y-%m-%d %H:%M:%S"
- Step 2: Commit
git add _config.yml
git commit -m "feat: update last_edit_time_format to YYYY-MM-DD HH:MM:SS"
Task 4: Python Script — Imports, Constants, Git Date Cache
Files:
-
Create:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write the file header, imports, and constants
#!/usr/bin/env python3
"""Add Just The Docs front matter to all content .md files.
Usage:
python add_front_matter.py --dry-run # Preview changes
python add_front_matter.py # Apply changes
"""
import argparse
import os
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
REPO_ROOT = Path(subprocess.check_output(
['git', 'rev-parse', '--show-toplevel'], text=True
).strip())
UNTOUCHED_FILES = {'CLAUDE.md'}
UNTOUCHED_DIRS = {'docs/superpowers'}
EXCLUDED_ROOT_FILES = {
'README.md', 'CONTRIBUTING.md', 'CONTRIBUTORS.md',
'STRUCTURE.md', 'CHANGELOG.md', 'bs-front-matter.md'
}
ROOT_NAV_ORDER = {
'.NET': 1, 'Architecture': 2, 'Data': 3, 'Web Services': 4,
'Azure': 5, 'Distributed Systems': 6, 'Testing': 7,
'JavaScript': 8, 'DevOps': 9
}
DEFAULT_DATE = '2026-03-30 00:00:00 +00:00'
# JTD supports max 3 navigation levels. Directories at depth 3+
# have their files flattened as children of the depth-2 ancestor.
MAX_NAV_DEPTH = 3
- Step 2: Write the git date cache function
def get_git_dates():
"""Run one bulk git log; return {relative_path: 'YYYY-MM-DD HH:MM:SS +00:00'}.
Parses `git log --format=COMMIT_DATE:<iso-date> --name-only` output.
Keeps only the first (most recent) date per file since git log is newest-first.
"""
result = subprocess.run(
['git', 'log', '--format=COMMIT_DATE:%aI', '--name-only', '--diff-filter=ACDMRT'],
capture_output=True, text=True, cwd=REPO_ROOT
)
dates = {}
current_date = None
for line in result.stdout.splitlines():
if line.startswith('COMMIT_DATE:'):
iso_str = line[len('COMMIT_DATE:'):]
dt = datetime.fromisoformat(iso_str).astimezone(timezone.utc)
current_date = dt.strftime('%Y-%m-%d %H:%M:%S') + ' +00:00'
elif line.strip() and current_date:
rel_path = line.strip().replace('\\', '/')
if rel_path not in dates:
dates[rel_path] = current_date
return dates
- Step 3: Verify git date cache works
Add a temporary test block at the bottom and run:
if __name__ == '__main__':
dates = get_git_dates()
print(f'Cached {len(dates)} file dates')
for path in sorted(dates)[:5]:
print(f' {path}: {dates[path]}')
Run: python docs/superpowers/scripts/add_front_matter.py
Expected: prints a count (100+) and 5 sample file→date mappings in YYYY-MM-DD HH:MM:SS +00:00 format.
Remove the temporary test block after verifying.
Task 5: Python Script — File Classification and Title Extraction
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write classify_file function
def classify_file(rel_path):
"""Return 'untouched', 'excluded', or 'jtd'."""
norm = rel_path.replace('\\', '/')
parts = norm.split('/')
filename = parts[-1]
if len(parts) == 1 and filename in UNTOUCHED_FILES:
return 'untouched'
for d in UNTOUCHED_DIRS:
if norm.startswith(d + '/'):
return 'untouched'
if len(parts) == 1 and filename in EXCLUDED_ROOT_FILES:
return 'excluded'
return 'jtd'
- Step 2: Write extract_title function
def extract_title(file_path):
"""Extract H1 heading from file content. Falls back to filename stem."""
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
m = re.match(r'^#\s+(.+)$', line.strip())
if m:
return m.group(1).strip()
return file_path.stem.replace('-', ' ').title()
Task 6: Python Script — Hierarchy Builder
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write build_hierarchy function
This function walks all .md files and determines parent, grand_parent, has_children, and depth for each JTD page. It handles three edge cases:
- Directories without
index.md(azure/lectures/, devops/os/) — files become children of the nearest ancestor index. - Depth-3 directories (data/nosql/mongo/) — files are flattened as level-3 children of the depth-2 ancestor.
- Root index.md — always
has_children: true, no parent.
def build_hierarchy(md_files):
"""Build hierarchy info for each JTD file.
Returns {rel_path: {parent, grand_parent, has_children, depth, nav_parent_dir}}.
nav_parent_dir is the directory whose index.md serves as this file's JTD parent.
"""
# Step 1: map directory -> index.md title
dir_titles = {}
for rel_path in md_files:
p = Path(rel_path)
if p.name == 'index.md' and classify_file(rel_path) == 'jtd':
d = str(p.parent).replace('\\', '/')
if d == '.':
d = ''
dir_titles[d] = extract_title(REPO_ROOT / rel_path)
# Step 2: determine hierarchy for each file
hierarchy = {}
for rel_path in md_files:
norm = rel_path.replace('\\', '/')
if classify_file(norm) != 'jtd':
continue
p = Path(norm)
file_dir = str(p.parent).replace('\\', '/')
if file_dir == '.':
file_dir = ''
depth = len(Path(file_dir).parts) if file_dir else 0
# Root index.md
if norm == 'index.md':
hierarchy[norm] = {
'parent': None, 'grand_parent': None,
'has_children': True, 'depth': 0, 'nav_parent_dir': None
}
continue
is_index = p.name == 'index.md'
if is_index:
# Index files: JTD parent is the parent directory's index
parent_dir = str(Path(file_dir).parent).replace('\\', '/')
if parent_dir == '.':
parent_dir = ''
parent_title = dir_titles.get(parent_dir)
# grand_parent: only if parent_dir itself has a parent with an index
grand_parent_title = None
if parent_dir:
gp_dir = str(Path(parent_dir).parent).replace('\\', '/')
if gp_dir == '.':
gp_dir = ''
gp_title = dir_titles.get(gp_dir)
# Only set grand_parent if the grandparent is NOT the root index
# (root-level pages have no parent in JTD nav)
if gp_title and gp_dir:
grand_parent_title = gp_title
# Depth-3+ index files: flatten — no has_children
if depth >= MAX_NAV_DEPTH:
hierarchy[norm] = {
'parent': parent_title, 'grand_parent': grand_parent_title,
'has_children': False, 'depth': depth,
'nav_parent_dir': parent_dir
}
continue
# Check if this index has children (other .md files in same dir or subdirs)
has_kids = False
for other in md_files:
other_norm = other.replace('\\', '/')
if other_norm == norm:
continue
if classify_file(other_norm) != 'jtd':
continue
other_dir = str(Path(other_norm).parent).replace('\\', '/')
if other_dir == file_dir or other_dir.startswith(file_dir + '/'):
has_kids = True
break
hierarchy[norm] = {
'parent': parent_title, 'grand_parent': grand_parent_title,
'has_children': has_kids, 'depth': depth,
'nav_parent_dir': parent_dir
}
else:
# Leaf files: parent is the index.md in their directory
# or nearest ancestor directory with an index.md
if file_dir in dir_titles:
nav_parent_dir = file_dir
else:
# No index.md here — walk up to find nearest ancestor
nav_parent_dir = str(Path(file_dir).parent).replace('\\', '/')
if nav_parent_dir == '.':
nav_parent_dir = ''
parent_title = dir_titles.get(nav_parent_dir)
# grand_parent
grand_parent_title = None
if nav_parent_dir:
gp_dir = str(Path(nav_parent_dir).parent).replace('\\', '/')
if gp_dir == '.':
gp_dir = ''
gp_title = dir_titles.get(gp_dir)
if gp_title and gp_dir:
grand_parent_title = gp_title
# Depth-3+ leaf files: flatten under depth-2 ancestor
if depth >= MAX_NAV_DEPTH:
# Walk up to find the depth-2 ancestor
ancestor_dir = file_dir
while len(Path(ancestor_dir).parts) >= MAX_NAV_DEPTH:
ancestor_dir = str(Path(ancestor_dir).parent).replace('\\', '/')
if ancestor_dir == '.':
ancestor_dir = ''
parent_title = dir_titles.get(ancestor_dir)
gp_dir = str(Path(ancestor_dir).parent).replace('\\', '/')
if gp_dir == '.':
gp_dir = ''
grand_parent_title = dir_titles.get(gp_dir) if gp_dir else None
nav_parent_dir = ancestor_dir
hierarchy[norm] = {
'parent': parent_title, 'grand_parent': grand_parent_title,
'has_children': False, 'depth': depth,
'nav_parent_dir': nav_parent_dir
}
return hierarchy
Task 7: Python Script — Nav Order Computation
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write compute_nav_order function
def compute_nav_order(md_files, hierarchy):
"""Compute nav_order for each JTD file.
Root index = 1. Category indices use ROOT_NAV_ORDER.
All other files: grouped by nav_parent_dir, sorted alphabetically by title, numbered from 1.
"""
nav_orders = {'index.md': 1}
# Category index files (depth-1 index.md) use ROOT_NAV_ORDER
for rel_path in md_files:
norm = rel_path.replace('\\', '/')
if norm not in hierarchy:
continue
info = hierarchy[norm]
if info['depth'] == 1 and Path(norm).name == 'index.md':
title = extract_title(REPO_ROOT / rel_path)
nav_orders[norm] = ROOT_NAV_ORDER.get(title, 99)
# All other files: group by nav_parent_dir, sort by title
groups = {}
for rel_path in md_files:
norm = rel_path.replace('\\', '/')
if norm in nav_orders or norm not in hierarchy:
continue
info = hierarchy[norm]
key = info.get('nav_parent_dir', '')
if key not in groups:
groups[key] = []
title = extract_title(REPO_ROOT / rel_path)
groups[key].append((title.lower(), title, norm))
for key, items in groups.items():
items.sort()
for i, (_, title, path) in enumerate(items, start=1):
nav_orders[path] = i
return nav_orders
Task 8: Python Script — Front Matter Generation
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write generate_front_matter function
def yaml_value(s):
"""Quote a YAML string value if it contains special characters."""
if any(c in s for c in "':{}[]|>&*!?,#"):
return f'"{s}"'
return s
def generate_front_matter(rel_path, hierarchy, nav_orders, git_dates):
"""Generate YAML front matter string for a file."""
norm = rel_path.replace('\\', '/')
classification = classify_file(norm)
if classification == 'excluded':
return '---\nnav_exclude: true\nsearch_exclude: true\n---\n\n'
if classification != 'jtd':
return ''
info = hierarchy.get(norm, {})
title = extract_title(REPO_ROOT / rel_path)
date = git_dates.get(norm, DEFAULT_DATE)
nav_order = nav_orders.get(norm, 1)
lines = ['---']
lines.append(f'title: {yaml_value(title)}')
if norm == 'index.md':
lines.append('layout: home')
elif Path(norm).name == 'index.md':
lines.append('layout: minimal')
else:
lines.append('layout: default')
lines.append(f'nav_order: {nav_order}')
if norm == 'index.md':
lines.append('description: "Dev documentation, tutorial, memory pointers, inverse oblivion"')
lines.append('permalink: /')
if info.get('has_children'):
lines.append('has_children: true')
if info.get('parent'):
lines.append(f'parent: {yaml_value(info["parent"])}')
if info.get('grand_parent'):
lines.append(f'grand_parent: {yaml_value(info["grand_parent"])}')
lines.append(f'last_modified_date: {date}')
lines.append('---')
lines.append('')
return '\n'.join(lines) + '\n'
Task 9: Python Script — Backlink Fixer
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write fix_backlinks function
Handles all observed backlink patterns:
[<<](./index.md) | [home](../README.md)— leaf files[<<](../README.md) | [home](../README.md)— category index files[<<](../index.md) | [home](../../README.md)— subcategory index files[<<](./index.md) | [home](/Tutorial/README/)— deep leaf files
def fix_backlinks(content, rel_path):
"""Replace old backlink patterns with new standardized format.
New format:
- Leaf files: [<](./index.md) | [<<](/index.md)
- Subcategory+ index files: [<](../index.md) | [<<](/index.md)
- Category index files (depth 1): [<<](/index.md) only
- Root index.md: no backlinks
- Files in dirs without index.md: [<](../index.md) | [<<](/index.md)
"""
norm = rel_path.replace('\\', '/')
if classify_file(norm) != 'jtd' or norm == 'index.md':
return content
p = Path(norm)
file_dir = str(p.parent).replace('\\', '/')
if file_dir == '.':
file_dir = ''
is_index = p.name == 'index.md'
depth = len(Path(file_dir).parts) if file_dir else 0
if is_index and depth == 1:
new_backlink = '[<<](/index.md)'
elif is_index:
new_backlink = '[<](../index.md) | [<<](/index.md)'
else:
has_local_index = (REPO_ROOT / file_dir / 'index.md').exists() if file_dir else False
if has_local_index:
new_backlink = '[<](./index.md) | [<<](/index.md)'
else:
new_backlink = '[<](../index.md) | [<<](/index.md)'
# Match: [<<](path) | [home](path) or [<<](path) | [<<](path)
pattern = r'\[<<?\]\([^)]+\)\s*\|\s*\[(?:home|<<?)\]\([^)]+\)'
new_content = re.sub(pattern, new_backlink, content)
return new_content
Task 10: Python Script — Main Entry Point
Files:
-
Modify:
docs/superpowers/scripts/add_front_matter.py -
Step 1: Write main function with argparse and dry-run support
def main():
parser = argparse.ArgumentParser(description='Add JTD front matter to .md files')
parser.add_argument('--dry-run', action='store_true',
help='Preview changes without modifying files')
args = parser.parse_args()
print('Caching git dates...')
git_dates = get_git_dates()
print(f' Cached dates for {len(git_dates)} files')
# Collect all .md files
md_files = []
for root, dirs, files in os.walk(REPO_ROOT):
dirs[:] = [d for d in dirs if not d.startswith('.')]
for f in files:
if f.endswith('.md'):
full = Path(root) / f
rel = str(full.relative_to(REPO_ROOT)).replace('\\', '/')
md_files.append(rel)
print(f'Found {len(md_files)} .md files')
hierarchy = build_hierarchy(md_files)
nav_orders = compute_nav_order(md_files, hierarchy)
stats = {'untouched': 0, 'excluded': 0, 'jtd': 0, 'skipped_has_fm': 0}
for rel_path in sorted(md_files):
norm = rel_path.replace('\\', '/')
classification = classify_file(norm)
if classification == 'untouched':
stats['untouched'] += 1
continue
file_path = REPO_ROOT / rel_path
content = file_path.read_text(encoding='utf-8')
# Skip files that already have front matter
if content.startswith('---\n'):
stats['skipped_has_fm'] += 1
if args.dry_run:
print(f' SKIP (has FM): {norm}')
continue
fm = generate_front_matter(norm, hierarchy, nav_orders, git_dates)
if not fm:
continue
new_content = fix_backlinks(content, norm)
final = fm + new_content
if args.dry_run:
print(f'\n--- {norm} [{classification}] ---')
print(fm.rstrip())
if new_content != content:
print(' [backlinks updated]')
else:
file_path.write_text(final, encoding='utf-8')
print(f' {classification}: {norm}')
stats[classification] += 1
print(f'\nSummary: untouched={stats["untouched"]}, excluded={stats["excluded"]}, '
f'jtd={stats["jtd"]}, skipped(has FM)={stats["skipped_has_fm"]}')
if __name__ == '__main__':
main()
- Step 2: Commit the script
git add docs/superpowers/scripts/add_front_matter.py
git commit -m "feat: add Python script for bulk JTD front matter"
Task 11: Dry-Run, Review, Execute
- Step 1: Run dry-run
python docs/superpowers/scripts/add_front_matter.py --dry-run
Expected output: front matter preview for each file, classification counts. Review for:
- Correct titles (extracted from H1)
- Correct hierarchy (parent/grand_parent match expected titles)
- Correct nav_order (root categories match README order, subfolders alphabetical)
- Correct dates (UTC format with +00:00)
- Backlink changes flagged where expected
- Excluded root files get only nav_exclude/search_exclude
- Untouched files (CLAUDE.md, docs/superpowers/**) are skipped
-
data/nosql/mongo/ files flattened under NoSQL (no level-4 nav)
- Step 2: Fix any issues found in dry-run review
If the dry-run output reveals problems, fix the script and re-run dry-run until clean.
- Step 3: Execute for real
python docs/superpowers/scripts/add_front_matter.py
- Step 4: Spot-check results
Verify front matter on these representative files:
head -15 index.md
head -10 dotnet/index.md
head -12 dotnet/parallelism/index.md
head -12 dotnet/parallelism/managed-threads.md
head -5 CONTRIBUTING.md
head -10 data/nosql/mongo/articles.md
tail -3 dotnet/parallelism/managed-threads.md
tail -3 dotnet/index.md
Check:
- Root index.md: layout home, nav_order 1, permalink /
- dotnet/index.md: layout minimal, nav_order 1, has_children true, no parent
- dotnet/parallelism/index.md: layout minimal, parent .NET, has_children true
- dotnet/parallelism/managed-threads.md: layout default, parent Parallelism, grand_parent .NET
- CONTRIBUTING.md: nav_exclude true, search_exclude true
- data/nosql/mongo/articles.md: parent NoSQL, grand_parent Data (flattened)
- Backlinks:
[<](./index.md) | [<<](/index.md)on leaf files -
Backlinks:
[<<](/index.md)on category index files - Step 5: Commit all front matter changes
git add -A
git commit -m "feat: add JTD front matter to all content pages and fix backlinks"
Task 12: Add nav_exclude to Remaining Root Files
The script handles CONTRIBUTING.md, CONTRIBUTORS.md, STRUCTURE.md, CHANGELOG.md, bs-front-matter.md. README.md was already done in Task 2.
- Step 1: Verify all excluded root files got front matter
head -5 CONTRIBUTING.md CONTRIBUTORS.md STRUCTURE.md CHANGELOG.md bs-front-matter.md
Each should start with:
---
nav_exclude: true
search_exclude: true
---
If any were missed (e.g. they already had --- at the top from other content), add the front matter manually.
- Step 2: Commit if any manual fixes were needed
git add CONTRIBUTING.md CONTRIBUTORS.md STRUCTURE.md CHANGELOG.md bs-front-matter.md
git commit -m "fix: ensure all excluded root files have nav_exclude front matter"
Task 13: Final Verification
- Step 1: Run a count check
grep -rl "^---" --include="*.md" . | wc -l
Expected: ~178 files (172 JTD pages + 6 excluded root files). Should NOT include CLAUDE.md or docs/superpowers/*/.md.
- Step 2: Verify untouched files are clean
head -3 CLAUDE.md
head -3 docs/superpowers/specs/2026-03-30-front-matter-design.md
Neither should start with --- front matter (unless CLAUDE.md already had content starting that way, which it doesn’t).
- Step 3: Local preview (optional)
jekyll serve
Open http://localhost:4000 and verify:
- Sidebar navigation shows the expected hierarchy
- Category pages list their children
- Backlinks work correctly
- “Last modified” dates display in the footer