Thread tag names from collect_tags() through generate_sitemap_file to generate_sitemap, appending /tags/<tag>.html entries to sitemap.xml output. Refactor: hoist collect_tags into run(), rename generate_tag_pages to write_tag_pages. Test suite: 83 → 84, phase 3 complete.
15 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 |
Simplest approach. 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 (minimum 12 new tests across 3 phases)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.xmlexists (default enabled)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 |
|---|
Retrospective
Process
- Did the plan hold up? Where did we diverge and why?
- Were the estimates/appetite realistic?
- Did CHALLENGE catch the risks that actually materialized?
Outcomes
- What unexpected debt was introduced?
- What would we do differently next cycle?
Pipeline Improvements
- Should any axiom/persona/workflow be updated based on this experience?
References
- Charter:
docs/charters/sukr-v1.md - Sketch:
.sketches/2026-02-13-api-stabilization.md