diff --git a/docs/plans/api-stabilization.md b/docs/plans/api-stabilization.md new file mode 100644 index 0000000..be9b28d --- /dev/null +++ b/docs/plans/api-stabilization.md @@ -0,0 +1,186 @@ +# 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) and `lightningcss` (alpha) are accepted risks — do not attempt to resolve +- `chrono` is acceptable for date validation — it's already a transitive dependency via `tera` +- 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 | Validate as YYYY-MM-DD string via `chrono::NaiveDate`, keep as `String` in template context | Validate at parse time; reject invalid formats. Feed generation already assumes this format. | +| 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/.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). `` redirect. | +| 404 page | `content/404.md` → `404.html` at output root | Simplest approach. Most static hosts auto-serve `/404.html`. | +| Template fallback | Try `section/.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: + +1. **YAML → TOML frontmatter switch.** The hand-rolled YAML parser can't handle `aliases` lists. Rather than extending a fragile parser, nrd decided to switch frontmatter to TOML — the `toml` crate 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: + +2. **`ConfigContext` normalization is clean.** Flat struct at `template_engine.rs:139-155`. Refactoring to nested `nav: NavContext { nested, toc }` is straightforward. +3. **`base_url` duplication confirmed.** `template_engine.rs:118` injects standalone `base_url`. `ConfigContext.base_url` at line 142 is canonical. Removal safe. +4. **Template variable `content` is correct.** Lines 57 and 83 inject rendered HTML as `content`, matching the sketch contract. +5. **Phase ordering is sound.** No circular dependencies between phases. +6. **17 content files need migration.** All simple frontmatter — no nested structures beyond `taxonomies.tags` (which becomes flat `tags = [...]`). 7 files also contain embedded YAML examples in body text that need updating. + +## Scope + +### In Scope + +1. **TOML frontmatter switch** — replace hand-rolled YAML parser with `#[derive(Deserialize)]` + `toml::from_str`, migrate 17 content files, change delimiter from `---` to `+++` +2. Template naming normalization (`config.nested_nav` → `config.nav.nested`, add `config.nav.toc`, remove duplicate `base_url`) +3. `draft` frontmatter field + filtering +4. `aliases` frontmatter field + redirect stub generation +5. Date validation (YYYY-MM-DD) at parse time +6. `[feed].enabled` and `[sitemap].enabled` config +7. `content/404.md` → `404.html` support +8. Tag listing page generation (`/tags/.html`) +9. Template section fallback (`section/.html` → `section/default.html`) +10. Dead template removal (`section/features.html`, `homepage.html`) +11. Tests for all new and changed behavior + +### Out of Scope + +- Pagination +- Asset co-location +- i18n +- Verbose/quiet CLI flags +- `collect_items()` caching +- `ContentKind` refactoring +- Magic string enum extraction + +## Phases + +1. **Phase 1: TOML Frontmatter & Config Normalization** — replace the parser, migrate content, fix naming + - [ ] Replace `Frontmatter` struct with `#[derive(Deserialize)]` + - [ ] Add new fields: `draft: bool` (`#[serde(default)]`), `aliases: Vec` (`#[serde(default)]`), keep all existing fields + - [ ] Replace `parse_frontmatter()` with `toml::from_str::()` + - [ ] Update `extract_frontmatter()` to detect `+++` delimiters instead of `---` + - [ ] Add date validation: custom deserializer or post-parse check for YYYY-MM-DD via `chrono::NaiveDate` + - [ ] Change `tags` from `taxonomies.tags` nesting to flat `tags = ["..."]` (direct TOML array) + - [ ] Migrate all 17 content files from YAML (`---`) to TOML (`+++`) frontmatter + - [ ] Update embedded frontmatter examples in documentation pages (7 files) + - [ ] Add `FeedConfig` and `SitemapConfig` structs to `config.rs` with `enabled: bool` (default `true`) + - [ ] Wire feed/sitemap config into `SiteConfig` deserialization + - [ ] Gate feed generation in `main.rs` on `config.feed.enabled` + - [ ] Gate sitemap generation in `main.rs` on `config.sitemap.enabled` + - [ ] Refactor `ConfigContext`: flat `nested_nav: bool` → nested `nav: NavContext { nested, toc }` + - [ ] Remove duplicate `base_url` top-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.html` and `docs/templates/homepage.html` + - [ ] Add template section fallback in `render_section`: try `section/.html`, fall back to `section/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) + +2. **Phase 2: Draft & Alias Features** — implement filtering and redirect generation + - [ ] Filter items where `draft == true` from `collect_items()` results + - [ ] Filter drafts from `SiteManifest.posts` during 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 (``) + - [ ] Add tests: draft filtering (excluded from listing, nav, feed, sitemap) + - [ ] Add tests: alias redirect generation (valid HTML, correct target URL) + +3. **Phase 3: 404 & Tag Pages** — new content generation features + - [ ] Detect `content/404.md` in content discovery, treat as special page + - [ ] Render `404.md` to `404.html` in output root + - [ ] Collect all unique tags across content items during build + - [ ] Create `tags/default.html` template in `docs/templates/` + - [ ] Generate `/tags/.html` for 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 + +## 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 warnings +- [ ] `cargo build` — clean compilation +- [ ] End-to-end: build `docs/` site with `cargo run`, verify: + - [ ] `public/sitemap.xml` exists (default enabled) + - [ ] `public/atom.xml` exists (default enabled) + - [ ] `public/404.html` exists (with 404.md in docs/content) + - [ ] Templates use `config.nav.nested` (not `config.nested_nav`) + - [ ] Templates use `config.base_url` (not bare `base_url`) + - [ ] No `section/features.html` or `homepage.html` templates 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`