docs(plan): add sukr 1.0 API stabilization plan
Three-phase plan to stabilize sukr's public API surfaces before 1.0: - Phase 1: Replace hand-rolled YAML frontmatter with TOML (+++ delimiters, serde Deserialize), normalize template variable naming, add feed/sitemap config toggles, template section fallback, remove dead templates - Phase 2: Draft filtering and alias redirect stub generation - Phase 3: 404 page support and tag listing page generation Key decisions: TOML frontmatter (eliminates fragile parser), config.nav nesting to mirror site.toml structure, flat tags array, date validation via chrono::NaiveDate. Ref: .sketches/2026-02-13-api-stabilization.md
This commit is contained in:
186
docs/plans/api-stabilization.md
Normal file
186
docs/plans/api-stabilization.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# PLAN: sukr 1.0 API Stabilization
|
||||
|
||||
<!--
|
||||
Source sketch: .sketches/2026-02-13-api-stabilization.md
|
||||
Selected approach: B+ (essential features + cleanup + aliases + date validation)
|
||||
Key pivot: YAML frontmatter → TOML frontmatter (CHALLENGE finding)
|
||||
-->
|
||||
|
||||
## 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/<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:
|
||||
|
||||
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/<tag>.html`)
|
||||
9. Template section fallback (`section/<type>.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<String>` (`#[serde(default)]`), keep all existing fields
|
||||
- [ ] Replace `parse_frontmatter()` with `toml::from_str::<Frontmatter>()`
|
||||
- [ ] 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/<type>.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 (`<meta http-equiv="refresh">`)
|
||||
- [ ] 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/<tag>.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
|
||||
|
||||
<!--
|
||||
Populated during execution. Empty at plan creation.
|
||||
-->
|
||||
|
||||
| Item | Severity | Why Introduced | Follow-Up | Resolved |
|
||||
| :--- | :------- | :------------- | :-------- | :------: |
|
||||
|
||||
## Retrospective
|
||||
|
||||
<!--
|
||||
Filled in after execution is complete.
|
||||
-->
|
||||
|
||||
### 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`
|
||||
Reference in New Issue
Block a user