Close plan review: fix Decisions table 404 naming (content/404.md → content/_404.md), check off Verification items, log collect_items() quadruple-call as technical debt, fill in Retrospective. Fix post-changeset staleness: AGENTS.md YAML→TOML reference, add [nav]/[feed]/[sitemap] config tables to AGENTS.md example, mark charter workstream 1 complete.
16 KiB
PLAN: sukr 1.0 API Stabilization
Goal
Implement the pre-1.0 API changes required to stabilize sukr's public contract: switch frontmatter from hand-rolled YAML to serde-backed TOML, normalize template variable naming, add missing features (draft mode, 404 page, tag listing pages, aliases, date validation, feed/sitemap config), remove dead code, and add template fallback behavior. After this work, the five public surfaces (site.toml schema, frontmatter fields, template variables, CLI, content directory conventions) are locked — post-1.0 breaking changes require explicit user approval.
Constraints
- Pre-1.0: breaking changes are acceptable now but the goal is to make them unnecessary after this work
- Suckless philosophy: no speculative features, no new dependencies unless already transitively present
tree-house(git dep) andlightningcss(alpha) are accepted risks — do not attempt to resolvechronois acceptable for date validation — it's already a transitive dependency viatera- Every surface stabilized is a surface committed to maintaining
Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Frontmatter format | TOML (replacing hand-rolled YAML) | toml crate + serde already in deps. Eliminates the fragile hand-rolled parser. Every future field is just a struct field with #[derive(Deserialize)]. |
| Frontmatter delimiter | +++ |
Hugo convention for TOML frontmatter. Unambiguous — no risk of confusion with YAML --- or Markdown horizontal rules. |
| Template naming | Mirror site.toml structure (config.nav.nested not config.nested_nav) |
Consistency between config and templates; pre-1.0 is only window for this break |
| Date type | Option<chrono::NaiveDate> with custom deserialize_date fn for TOML native dates |
Parse, don't validate. Custom serde deserializer accepts toml::Datetime, extracts date, constructs NaiveDate. Invalid dates fail at deser. |
| Draft filtering | draft: bool (#[serde(default)]) in Frontmatter, filter in collect_items() and discover() |
Filter early so drafts don't appear in nav, listings, sitemap, or feed. |
| Feed/sitemap config | [feed] and [sitemap] tables with enabled boolean in SiteConfig |
Users need opt-out. Default true preserves backward compat. |
| Tag listing pages | Generate /tags/<tag>.html using a new tags/default.html template |
Minimal approach — one template, one generation loop. No pagination. |
| Aliases | aliases = ["/old/path"] in frontmatter, generate HTML redirect stubs |
Standard pattern (Hugo). <meta http-equiv="refresh"> redirect. |
| 404 page | content/_404.md → 404.html at output root |
Underscore prefix convention (matches _index.md). Most static hosts auto-serve /404.html. |
| Template fallback | Try section/<type>.html, fall back to section/default.html |
Removes the requirement to create a template for every section_type. |
| Dead template cleanup | Delete section/features.html and homepage.html |
Byte-for-byte duplicate and dead code respectively. |
base_url duplication |
Remove top-level base_url template variable |
Single source of truth via config.base_url. |
| Tags syntax | tags = ["foo", "bar"] (flat TOML array) |
Replaces nested taxonomies.tags YAML. Simpler, no indirection. |
Risks & Assumptions
| Risk / Assumption | Severity | Status | Mitigation / Evidence |
|---|---|---|---|
| TOML frontmatter breaks all 17 existing content files | HIGH | Validated | One-time mechanical migration. All files are simple key-value; no complex structures. Migration is part of Phase 1. |
| Documentation pages embed YAML frontmatter examples | MEDIUM | Validated | configuration.md, content-organization.md, getting-started.md, feeds.md, sections.md, sitemap.md, templates.md all contain example frontmatter in their body text. Must update these doc examples too. |
| Template naming break affects existing user templates | MEDIUM | Validated | Only docs/templates/base.html references config.nested_nav and base_url. The docs site is the only known deployment. |
| Tag listing pages need a template but no default ships | MEDIUM | Unvalidated | Must design and ship tags/default.html in docs/templates/. |
collect_items() triple-call not fixed by this plan |
LOW | Accepted | Performance issue, not API concern. Deferred. |
Removing base_url top-level variable breaks templates |
MEDIUM | Validated | Only docs/templates/base.html uses it. |
Open Questions
All resolved during CHALLENGE. See CHALLENGE Notes below.
CHALLENGE Notes
Items presented to nrd for decision:
- YAML → TOML frontmatter switch. The hand-rolled YAML parser can't handle
aliaseslists. Rather than extending a fragile parser, nrd decided to switch frontmatter to TOML — thetomlcrate and serde are already dependencies. This eliminates the parser entirely and replaces 70 lines of hand-rolled code with#[derive(Deserialize)]. Decision: switch to TOML.
Items validated by codebase investigation:
ConfigContextnormalization is clean. Flat struct attemplate_engine.rs:139-155. Refactoring to nestednav: NavContext { nested, toc }is straightforward.base_urlduplication confirmed.template_engine.rs:118injects standalonebase_url.ConfigContext.base_urlat line 142 is canonical. Removal safe.- Template variable
contentis correct. Lines 57 and 83 inject rendered HTML ascontent, matching the sketch contract. - Phase ordering is sound. No circular dependencies between phases.
- 17 content files need migration. All simple frontmatter — no nested structures beyond
taxonomies.tags(which becomes flattags = [...]). 7 files also contain embedded YAML examples in body text that need updating.
Scope
In Scope
- TOML frontmatter switch — replace hand-rolled YAML parser with
#[derive(Deserialize)]+toml::from_str, migrate 17 content files, change delimiter from---to+++ - Template naming normalization (
config.nested_nav→config.nav.nested, addconfig.nav.toc, remove duplicatebase_url) draftfrontmatter field + filteringaliasesfrontmatter field + redirect stub generation- Date validation (YYYY-MM-DD) at parse time
[feed].enabledand[sitemap].enabledconfigcontent/_404.md→404.htmlsupport- Tag listing page generation (
/tags/<tag>.html) - Template section fallback (
section/<type>.html→section/default.html) - Dead template removal (
section/features.html,homepage.html) - Tests for all new and changed behavior
Out of Scope
- Pagination
- Asset co-location
- i18n
- Verbose/quiet CLI flags
collect_items()cachingContentKindrefactoring- Magic string enum extraction
Phases
-
Phase 1: TOML Frontmatter & Config Normalization — replace the parser, migrate content, fix naming
- Replace
Frontmatterstruct with#[derive(Deserialize)] - Add new fields:
draft: bool(#[serde(default)]),aliases: Vec<String>(#[serde(default)]), keep all existing fields - Replace
parse_frontmatter()withtoml::from_str::<Frontmatter>() - Update
extract_frontmatter()to detect+++delimiters instead of--- - Add date validation: custom
deserialize_datefn for TOML native dates →chrono::NaiveDate - Change
tagsfromtaxonomies.tagsnesting to flattags = ["..."](direct TOML array) - Migrate all 17 content files from YAML (
---) to TOML (+++) frontmatter - Update embedded frontmatter examples in documentation pages (7 files)
- Add
FeedConfigandSitemapConfigstructs toconfig.rswithenabled: bool(defaulttrue) - Wire feed/sitemap config into
SiteConfigdeserialization - Gate feed generation in
main.rsonconfig.feed.enabled - Gate sitemap generation in
main.rsonconfig.sitemap.enabled - Refactor
ConfigContext: flatnested_nav: bool→ nestednav: NavContext { nested, toc } - Remove duplicate
base_urltop-level template variable injection - Update
docs/templates/base.html:config.nested_nav→config.nav.nested,base_url→config.base_url - Delete
docs/templates/section/features.htmlanddocs/templates/homepage.html - Add template section fallback in
render_section: trysection/<type>.html, fall back tosection/default.html - Update/fix all existing tests to use TOML frontmatter
- Add new tests: TOML parsing, date validation (valid + invalid), feed/sitemap config gating
- Verify all 69 existing tests pass (updated for TOML)
- Replace
-
Phase 2: Draft & Alias Features — implement filtering and redirect generation
- Filter items where
draft == truefromcollect_items()results - Filter drafts from
SiteManifest.postsduring discovery - Filter drafts from nav discovery (
discover_nav()) - Filter drafts from sitemap entries
- Filter drafts from feed entries
- Generate HTML redirect stubs for each alias path (
<meta http-equiv="refresh">) - Add tests: draft filtering (excluded from listing, nav, feed, sitemap)
- Add tests: alias redirect generation (valid HTML, correct target URL)
- Filter items where
-
Phase 3: 404 & Tag Pages — new content generation features
- Detect
content/_404.mdin content discovery, treat as special page - Render
_404.mdto404.htmlin output root - Collect all unique tags across content items during build
- Create
tags/default.htmltemplate indocs/templates/ - Generate
/tags/<tag>.htmlfor each unique tag with list of tagged items - Add tag listing page entries to sitemap (if enabled)
- Add tests: 404 page generation
- Add tests: tag listing page generation (correct paths, correct items per tag)
- End-to-end: build
docs/site and verify all outputs
- Detect
Verification
cargo test— all existing tests pass (updated for TOML frontmatter)cargo test— all new tests pass (16 new tests across 3 phases: 69 → 84)cargo clippy -- -D warnings— no warningscargo build— clean compilation- End-to-end: build
docs/site withcargo run, verify:public/sitemap.xmlexists (default enabled)public/atom.xml— N/A (no blog sections in docs site; feed gating verified by unit tests)public/404.htmlexists (with _404.md in docs/content)- Templates use
config.nav.nested(notconfig.nested_nav) - Templates use
config.base_url(not barebase_url) - No
section/features.htmlorhomepage.htmltemplates remain
Technical Debt
| Item | Severity | Why Introduced | Follow-Up | Resolved |
|---|---|---|---|---|
collect_items() called 4× per build |
LOW | Tag collection (commit 7) and alias generation (commit 6) each added a new call site, growing the original 2-call pattern to 4 | Cache section items in SiteManifest — deferred to Error Hardening or Performance pass |
☐ |
Retrospective
Process
The plan held up without requiring replanning during execution. The YAML → TOML pivot — the single largest decision — was identified during CHALLENGE (in /plan), not mid-execution. Once baked into the plan, execution was linear across all 3 phases.
Phase sequencing was sound: Phase 1 (parser + config) → Phase 2 (features depending on new fields) → Phase 3 (independent new features). No circular dependencies materialized.
Estimates were accurate: 3 phases, 8 commits predicted and delivered. CHALLENGE caught the risks that materialized — the 17-file content migration (flagged HIGH) was the most labor-intensive step but proceeded mechanically.
Outcomes
Unexpected debt: collect_items() grew from 2 to 4 call sites. The original plan flagged the triple-call as "Accepted — LOW" but execution added a fourth. Logged in Technical Debt above.
What we'd do differently: Commit 3 (45448cc) bundled 4 concerns (config refactor, base_url dedup, dead template deletion, section fallback). Tests for that commit's new behavior landed in commit 4. A 2-commit split would have maintained stricter atomicity without being excessive.
Pipeline Improvements
- AGENTS.md has stale fragment paths (
.agent/predicates/fragments/→.agent/personas/). Discovered during predicate refresh — should be fixed independently. /plan-reviewshould remind agents to mark Verification checkboxes during execution, not defer to review time.
References
- Charter:
docs/charters/sukr-v1.md - Sketch:
.sketches/2026-02-13-api-stabilization.md