kitfly 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +25 -10
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/guide/branding.md +146 -0
  5. package/dist/_raw/content/guide/data-driven-content.md +204 -0
  6. package/dist/_raw/content/reference/configuration.md +145 -7
  7. package/dist/_raw/content/reference/environment-variables.md +26 -1
  8. package/dist/_raw/content/reference/glossary.md +25 -1
  9. package/dist/_raw/content/reference/key-concepts.md +30 -2
  10. package/dist/_raw/content/reference/plugins.md +14 -0
  11. package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
  12. package/dist/content/deployment/preflight.html +10 -6
  13. package/dist/content/deployment/recipes/aws-s3.html +10 -6
  14. package/dist/content/deployment/recipes/cloudflare-pages.html +10 -6
  15. package/dist/content/deployment/recipes/cloudflare-r2.html +10 -6
  16. package/dist/content/deployment/recipes/fly-io.html +10 -6
  17. package/dist/content/deployment/recipes/github-pages.html +10 -6
  18. package/dist/content/deployment/recipes/netlify.html +10 -6
  19. package/dist/content/deployment/recipes/vercel.html +10 -6
  20. package/dist/content/deployment/secrets-and-env-vars.html +10 -6
  21. package/dist/content/deployment.html +10 -6
  22. package/dist/content/guide/approaches.html +10 -6
  23. package/dist/content/guide/branding.html +510 -0
  24. package/dist/content/guide/data-driven-content.html +543 -0
  25. package/dist/content/guide/features.html +10 -6
  26. package/dist/content/guide/getting-started.html +10 -6
  27. package/dist/content/guide/kitfly-overview.html +10 -6
  28. package/dist/content/reference/configuration.html +135 -9
  29. package/dist/content/reference/design-catalog.html +10 -6
  30. package/dist/content/reference/environment-variables.html +50 -8
  31. package/dist/content/reference/glossary.html +24 -8
  32. package/dist/content/reference/key-concepts.html +33 -9
  33. package/dist/content/reference/plugins.html +22 -7
  34. package/dist/content/reference/slides-authoring-guidelines.html +10 -6
  35. package/dist/content/reference/structure.html +10 -6
  36. package/dist/content/reference.html +10 -6
  37. package/dist/content/templates/crucible.html +10 -6
  38. package/dist/content/templates/handbook.html +10 -6
  39. package/dist/content/templates/minimal.html +10 -6
  40. package/dist/content/templates/overview.html +10 -6
  41. package/dist/content/templates/pipeline.html +10 -6
  42. package/dist/content/templates/productbook.html +10 -6
  43. package/dist/content/templates/runbook.html +10 -6
  44. package/dist/content/templates/servicebook.html +10 -6
  45. package/dist/content-index.json +29 -2
  46. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +10 -6
  47. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +10 -6
  48. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +10 -6
  49. package/dist/docs/decisions/ADR-0004-bun-runtime.html +10 -6
  50. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +10 -6
  51. package/dist/docs/decisions/ADR-0006-data-driven-content.html +752 -0
  52. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +10 -6
  53. package/dist/docs/decisions/DDR-0002-theme-system.html +10 -6
  54. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +10 -6
  55. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +10 -6
  56. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +10 -6
  57. package/dist/docs/userguide/cli/build.html +10 -6
  58. package/dist/docs/userguide/cli/bundle.html +10 -6
  59. package/dist/docs/userguide/cli/dev.html +10 -6
  60. package/dist/docs/userguide/cli/init.html +10 -6
  61. package/dist/docs/userguide/cli/servers.html +10 -6
  62. package/dist/docs/userguide/cli/stop.html +10 -6
  63. package/dist/docs/userguide/cli/update.html +10 -6
  64. package/dist/docs/userguide/cli/version.html +10 -6
  65. package/dist/docs/userguide/cli.html +10 -6
  66. package/dist/docs/userguide/sharing.html +10 -6
  67. package/dist/index.html +10 -6
  68. package/dist/llms.txt +3 -3
  69. package/dist/provenance.json +4 -4
  70. package/dist/schemas/plugin-registry.schema.html +10 -6
  71. package/dist/schemas/plugin-schemas-notes.html +10 -6
  72. package/dist/schemas/plugin.schema.html +10 -6
  73. package/dist/schemas/plugins.schema.html +10 -6
  74. package/dist/schemas/v0/common.schema.html +14 -10
  75. package/dist/schemas/v0/plugin-registry.schema.html +13 -9
  76. package/dist/schemas/v0/plugin.schema.html +13 -9
  77. package/dist/schemas/v0/plugins.schema.html +13 -9
  78. package/dist/schemas/v0/site.schema.html +67 -7
  79. package/dist/schemas/v0/theme.schema.html +21 -17
  80. package/dist/schemas.html +10 -6
  81. package/dist/styles.css +39 -4
  82. package/package.json +1 -1
  83. package/plugins-dist/latex-runtime.js +140 -0
  84. package/plugins-dist/latex.js +178 -0
  85. package/plugins-dist/slides-charts-lite-runtime.js +179 -0
  86. package/plugins-dist/slides-charts-lite.js +198 -0
  87. package/registry/plugins.yaml +25 -0
  88. package/schemas/v0/site.schema.json +56 -0
  89. package/scripts/build.ts +191 -69
  90. package/scripts/bundle.ts +118 -10
  91. package/scripts/dev.ts +245 -166
  92. package/src/__tests__/brief.test.ts +151 -0
  93. package/src/__tests__/build.test.ts +169 -1
  94. package/src/__tests__/bundle.test.ts +134 -0
  95. package/src/__tests__/init.test.ts +51 -2
  96. package/src/__tests__/latex-runtime.bun.test.ts +35 -0
  97. package/src/__tests__/shared.test.ts +598 -1
  98. package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
  99. package/src/cli.ts +11 -4
  100. package/src/commands/init.ts +1 -1
  101. package/src/shared.ts +725 -18
  102. package/src/site/styles.css +39 -4
  103. package/src/site/template.html +5 -2
  104. package/src/templates/brief.ts +486 -0
  105. package/src/templates/deck.ts +59 -0
  106. package/src/templates/driver.ts +46 -13
  107. package/src/templates/handbook.ts +32 -0
  108. package/src/templates/runbook.ts +32 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  All notable changes to Kitfly are documented here.
4
4
 
5
+ ## [0.2.3] - 2026-02-17
6
+
7
+ ### Added
8
+
9
+ - Content profiles: `--profile` flag and `KITFLY_PROFILE` env var for single-source multi-audience filtering via frontmatter `profile:` tags
10
+ - Data-driven bindings: `{{ key }}` value substitution and `{{ snippet:name }}` block injection from YAML/JSON data files bound via `data:` frontmatter
11
+ - Pre-build hooks: `prebuild:` commands in site.yaml that run before dev/build/bundle with watch-mode reruns
12
+ - Built-in formatters: `dollar`, `number`, `percent`, `round(n)`, `upper`, `lower` with pipe chaining (`{{ key | round(0) | dollar }}`)
13
+ - Optional schema validation for data files (JSON Schema structural checks)
14
+ - ADR-0006: Data-Driven Content architecture decision
15
+ - "Kitsite" terminology and "What's a Kitsite?" section in README
16
+
17
+ ### Fixed
18
+
19
+ - Windows cross-platform compatibility: launcher script install (no symlinks), browser open dispatch, MSYS `/c/...` path normalization
20
+ - Profile filtering backward compatibility when no profiles configured and no active profile selected
21
+ - `KITFLY_PROFILE` environment variable propagation through kitfly CLI entrypoints
22
+ - YAML parser: block scalar (`|`/`>`) support for data file snippets
23
+ - YAML parser: direct list-item block scalars, chomping/indent indicator handling
24
+ - YAML parser: reject malformed block scalar headers (`|abc`, `>foo`) instead of silent empty strings
25
+
26
+ ### Docs
27
+
28
+ - Windows contributor setup guide in docs/development.md
29
+ - Development docs table alignment fix
30
+
31
+ ### Generator guidance (from dogfooding)
32
+
33
+ - `pages[].path` in data files must be relative to site root including `content/` prefix (e.g. `content/product/pricing.md`, not `product/pricing.md`)
34
+ - Generators must emit every snippet the template references, even if empty — the template is the contract
35
+ - `percent` formatter expects a decimal ratio (0.0–1.0), not an already-computed percentage
36
+ - Use JSON for generator-produced data files; reserve YAML for hand-authored data where readability matters
37
+ - Recommended layout: raw input in `data/raw/`, kitfly data files in `data/`
38
+
39
+ ## [0.2.2] - 2026-02-16
40
+
41
+ ### Added
42
+
43
+ - `slides-charts-lite` plugin: bar/line/pie charts via Chart.js 4.4.7 CDN with fenced `chart` code blocks
44
+ - `latex` plugin: math typesetting via KaTeX 0.16.21 CDN ($inline$, $$display$$, fenced `math` blocks)
45
+ - `brief` template: external-audience product documentation with 4 sections and starter content
46
+ - Footer logo: `footer.logo`, `logoUrl`, `logoAlt`, `logoHeight` fields for image logos in the footer ribbon
47
+ - Dark mode logo variants: `brand.logoDark` and `footer.logoDark` fields with CSS show/hide swap (no JS)
48
+ - Hierarchical slide navigation: nested nav with collapsible groups for decks with subfolder content
49
+ - Branding guide (`content/guide/branding.md`): canonical reference for header logos, footer logos, dark mode variants
50
+
51
+ ### Fixed
52
+
53
+ - Plugin template injection: `$` replacement corruption that broke LaTeX delimiter rendering
54
+ - Plugin CDN loader: path resolution for LaTeX plugin assets
55
+
56
+ ### Docs
57
+
58
+ - Configuration reference updated with footer logo and dark mode logo fields
59
+ - All templates (handbook, runbook, brief, deck) updated with expanded Brand Assets documentation
60
+
5
61
  ## [0.2.1] - 2026-02-15
6
62
 
7
63
  ### Added
package/README.md CHANGED
@@ -31,21 +31,36 @@ Alternative: `npm install -g kitfly` (still requires Bun, because the CLI runs w
31
31
 
32
32
  No global install: `bunx kitfly --version`
33
33
 
34
- Note: `bun`/`npm` installs the latest published release. If youre reading `main` before a release is cut, clone this repo for the newest features.
34
+ Note: `bun`/`npm` installs the latest published release. If you're reading `main` before a release is cut, clone this repo for the newest features.
35
+
36
+ **Don't need the CLI?** `kitfly init` creates a [standalone site](#create-a-standalone-site-recommended) that runs with just Bun — no kitfly binary required after setup.
35
37
 
36
38
  For contributor/development setup, see [docs/development.md](docs/development.md).
37
39
 
38
40
  ---
39
41
 
42
+ ## What's a Kitsite?
43
+
44
+ Any site created or managed by kitfly is a **kitsite** — a self-contained workspace with your content, configuration, and (optionally) build scripts. A kitsite can operate in different **modes**:
45
+
46
+ | Mode | What it produces | Created with |
47
+ | -------- | --------------------------------- | ------------------------------------- |
48
+ | `docs` | Documentation site (default) | `kitfly init my-docs` |
49
+ | `slides` | Fixed-aspect slide deck (v0.2.0+) | `kitfly init my-deck --template deck` |
50
+
51
+ Every kitsite has the same structure: a `content/` directory with your markdown, a `site.yaml` for configuration, and static output you can host anywhere or bundle into a single HTML file. Standalone kitsites (created with `kitfly init`) include their own build scripts and run with just Bun — no kitfly CLI required after setup.
52
+
53
+ ---
54
+
40
55
  ## Three Ways to Use Kitfly
41
56
 
42
- | Approach | Best For | What You Get |
43
- | ------------------------- | ------------- | ---------------------------------------------- |
44
- | **`kitfly init`** | New projects | Standalone site with your own copy of the code |
45
- | **`kitfly dev ./folder`** | Existing docs | Quick preview without changing anything |
46
- | **Clone this repo** | Contributors | The kitfly engine itself |
57
+ | Approach | Best For | What You Get |
58
+ | ------------------------- | ------------- | ------------------------------------------------- |
59
+ | **`kitfly init`** | New projects | Standalone kitsite with your own copy of the code |
60
+ | **`kitfly dev ./folder`** | Existing docs | Quick preview without changing anything |
61
+ | **Clone this repo** | Contributors | The kitfly engine itself |
47
62
 
48
- ### Create a Standalone Site (Recommended)
63
+ ### Create a Standalone Kitsite (Recommended)
49
64
 
50
65
  ```bash
51
66
  kitfly init my-docs
@@ -63,7 +78,7 @@ bun install
63
78
  bun run dev
64
79
  ```
65
80
 
66
- You get a complete, self-contained site: rendering code, template, styles, config. It's yours — modify it, version it, own it. No kitfly CLI required after setup.
81
+ You get a complete, self-contained kitsite: rendering code, template, styles, config. It's yours — modify it, version it, own it. No kitfly CLI required after setup.
67
82
 
68
83
  ### Preview Existing Docs
69
84
 
@@ -149,7 +164,7 @@ Or just drop markdown files in `content/` — sections auto-discover.
149
164
 
150
165
  ## Plugins
151
166
 
152
- Plugins are optional CSS/JS add-ons (kept out of core) that you enable per-site.
167
+ Plugins are optional CSS/JS add-ons (kept out of core) that you enable per-kitsite.
153
168
 
154
169
  - Config: `kitfly.plugins.yaml`
155
170
  - Versions are pinned (`name@x.y.z`)
@@ -163,7 +178,7 @@ See `content/reference/plugins.md` for the contract and examples.
163
178
 
164
179
  > **Rule of thumb**: If it can't be done with CSS, vanilla JS under 50 lines, or a marked plugin, it doesn't belong here.
165
180
 
166
- Kitfly is intentionally limited. The goal is a doc site that stays simple and maintainable. When you outgrow it, migrate — your content is just markdown.
181
+ Kitfly is intentionally limited. The goal is a kitsite that stays simple and maintainable. When you outgrow it, migrate — your content is just markdown.
167
182
 
168
183
  ---
169
184
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.3
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Branding
3
+ description: Configure header logos, footer logos, and dark mode variants
4
+ ---
5
+
6
+ # Branding
7
+
8
+ Kitfly sites display your brand in two locations: the **header** (sidebar logo + site title) and the **footer ribbon** (optional logo, copyright, links). Both support light/dark mode variants.
9
+
10
+ ## Header Logo
11
+
12
+ The header logo appears in the sidebar above the navigation. Configure it in `site.yaml`:
13
+
14
+ ```yaml
15
+ brand:
16
+ name: "My Project"
17
+ url: "/"
18
+ logo: "assets/brand/logo.png"
19
+ ```
20
+
21
+ The logo renders inside a bounded slot that preserves aspect ratio. Both square icons and wide wordmarks work — set `logoType` to tell kitfly which you're using:
22
+
23
+ ```yaml
24
+ brand:
25
+ logo: "assets/brand/logo.png"
26
+ logoType: "icon" # square mark (default)
27
+ ```
28
+
29
+ ```yaml
30
+ brand:
31
+ logo: "assets/brand/wordmark.svg"
32
+ logoType: "wordmark" # wide logo
33
+ ```
34
+
35
+ | Breakpoint | Max Height | Max Width |
36
+ | ---------------- | ---------- | --------- |
37
+ | Desktop | 64px | 180px |
38
+ | Tablet (≤1024px) | 56px | 150px |
39
+ | Mobile (≤768px) | 48px | 130px |
40
+
41
+ SVG and PNG formats are supported. For SVGs, ensure the `viewBox` is tightly cropped to the artwork.
42
+
43
+ If the logo image is missing or fails to load, kitfly falls back to displaying the first letter of your brand name.
44
+
45
+ ## Footer Logo
46
+
47
+ Add a logo to the footer ribbon — typically a parent company, client, or partner logo that differs from the header brand:
48
+
49
+ ```yaml
50
+ footer:
51
+ logo: "assets/brand/footer-logo.png"
52
+ ```
53
+
54
+ The footer logo renders at the leading edge of the ribbon, before the version and publish date.
55
+
56
+ ### Footer Logo Options
57
+
58
+ | Field | Default | Description |
59
+ | ------------ | ---------------------------- | ----------------------------------- |
60
+ | `logo` | _(none)_ | Path to footer logo image |
61
+ | `logoUrl` | _(none)_ | Make the logo a clickable link |
62
+ | `logoAlt` | Copyright text or brand name | Alt text for accessibility |
63
+ | `logoHeight` | `20` | Max height in pixels (range: 10-40) |
64
+
65
+ ```yaml
66
+ footer:
67
+ logo: "assets/brand/footer-logo.png"
68
+ logoUrl: "https://example.com"
69
+ logoAlt: "Example Corp"
70
+ logoHeight: 24
71
+ ```
72
+
73
+ If no `footer.logo` is set, the footer renders as text only (version, copyright, links, attribution) — no change from the default.
74
+
75
+ ## Dark Mode Variants
76
+
77
+ By default, kitfly auto-adjusts logo brightness in dark mode using a CSS filter. This works for many logos but can distort brand colors or fail on dark logos with transparent backgrounds.
78
+
79
+ For precise control, provide a separate dark mode image:
80
+
81
+ ### Header
82
+
83
+ ```yaml
84
+ brand:
85
+ logo: "assets/brand/logo.png"
86
+ logoDark: "assets/brand/logo-dark.png"
87
+ ```
88
+
89
+ ### Footer
90
+
91
+ ```yaml
92
+ footer:
93
+ logo: "assets/brand/footer-logo.png"
94
+ logoDark: "assets/brand/footer-logo-dark.png"
95
+ ```
96
+
97
+ ### How It Works
98
+
99
+ When `logoDark` is set, kitfly emits both images and uses CSS to show the correct one based on the active theme. No JavaScript is involved — the swap is instant on theme toggle.
100
+
101
+ | Configuration | Light Mode | Dark Mode |
102
+ | ------------------- | ---------- | --------------------------------- |
103
+ | `logo` only | Shows logo | Shows logo with brightness filter |
104
+ | `logo` + `logoDark` | Shows logo | Shows logoDark (no filter) |
105
+
106
+ ### Recommendations
107
+
108
+ - Use the **single logo** approach when your logo works on both light and dark backgrounds (e.g. a colorful icon on transparent background)
109
+ - Use **light + dark variants** when your logo has a specific background assumption (e.g. dark wordmark that disappears on dark backgrounds)
110
+ - Keep both variants at the **same dimensions** so the layout doesn't shift on theme toggle
111
+ - SVG is ideal for both variants — resolution-independent and small file size
112
+
113
+ ## Recommended Asset Files
114
+
115
+ | Asset | Location | Size / Format |
116
+ | ------------------- | ----------------------------------- | ------------------- |
117
+ | Logo (light) | `assets/brand/logo.png` | 200x50px or SVG |
118
+ | Logo (dark) | `assets/brand/logo-dark.png` | Same as logo |
119
+ | Footer logo (light) | `assets/brand/footer-logo.png` | Max height 20-40px |
120
+ | Footer logo (dark) | `assets/brand/footer-logo-dark.png` | Same as footer logo |
121
+ | Favicon | `assets/brand/favicon.ico` | 32x32px |
122
+
123
+ ## Full Example
124
+
125
+ A site with separate header and footer logos, both with dark mode variants:
126
+
127
+ ```yaml
128
+ # site.yaml
129
+ title: "Product Brief"
130
+
131
+ brand:
132
+ name: "Product Name"
133
+ url: "/"
134
+ logo: "assets/brand/product-logo.png"
135
+ logoDark: "assets/brand/product-logo-dark.png"
136
+ logoType: "wordmark"
137
+
138
+ footer:
139
+ copyright: "© 2026 Parent Company, Inc."
140
+ copyrightUrl: "https://example.com"
141
+ logo: "assets/brand/parent-logo.png"
142
+ logoDark: "assets/brand/parent-logo-dark.png"
143
+ logoAlt: "Parent Company"
144
+ logoHeight: 20
145
+ attribution: true
146
+ ```
@@ -0,0 +1,204 @@
1
+ ---
2
+ title: "Data-Driven Content"
3
+ description: "Use data bindings and generators to build pages from structured data"
4
+ last_updated: "2026-02-17"
5
+ ---
6
+
7
+ # Data-Driven Content
8
+
9
+ Some pages are driven by structured data — pricing tables, team rosters, metrics dashboards, product catalogs. Data-driven content lets you separate prose (markdown), computation (generators), and data (YAML/JSON files) so each layer does what it's good at.
10
+
11
+ ## When to use this
12
+
13
+ Use data bindings when:
14
+
15
+ - The same values appear in multiple places on a page (DRY)
16
+ - A generator computes values that appear in prose (pricing math, discount brackets)
17
+ - Tables or blocks are produced by scripts and injected into narrative content
18
+
19
+ Don't use data bindings when:
20
+
21
+ - The page is pure prose with no external data
22
+ - You need loops, conditionals, or expressions — use a generator to produce the final markdown instead
23
+
24
+ ## How it works
25
+
26
+ ```
27
+ raw input → generator → data file → kitfly bindings → markdown → HTML
28
+ ```
29
+
30
+ 1. A **generator** (any script) reads raw input and writes a data file
31
+ 2. A **data file** (YAML or JSON) declares globals, per-page values, and snippets
32
+ 3. **Markdown templates** use `{{ key }}` and `{{ snippet:name }}` placeholders
33
+ 4. Kitfly resolves bindings at build time, before markdown rendering
34
+
35
+ ## Data file structure
36
+
37
+ ```yaml
38
+ # data/pricing.yaml
39
+ globals:
40
+ company: "Acme Corp"
41
+ baseline_rate: "200"
42
+ credit_validity: "12"
43
+
44
+ pages:
45
+ - path: content/product/pricing.md
46
+ inject:
47
+ hero: "Implementation and operating costs"
48
+ discount_range: "5–20%"
49
+ snippets:
50
+ - slot: pricing-table
51
+ content: |
52
+ | Tier | Price | Features |
53
+ |------|-------|----------|
54
+ | Basic | $10/mo | Essentials |
55
+ | Pro | $50/mo | Full access |
56
+ ```
57
+
58
+ **Path convention:** `pages[].path` must be relative to site root including the `content/` prefix. Use `content/product/pricing.md`, not `product/pricing.md`. Kitfly's error messages include the expected path if there's a mismatch.
59
+
60
+ ### Resolution order
61
+
62
+ For `{{ key }}`: page `inject` first, then `globals`. Page-level values shadow globals on key collision.
63
+
64
+ For `{{ snippet:name }}`: matched by `slot` name from the page's `snippets` array.
65
+
66
+ ## Binding syntax
67
+
68
+ ### Value substitution
69
+
70
+ ```markdown
71
+ Implementation rate: **{{ baseline_rate | dollar }}/hour**
72
+
73
+ Credits valid for {{ credit_validity }} months.
74
+ ```
75
+
76
+ ### Snippet injection
77
+
78
+ ```markdown
79
+ ## Pricing Tiers
80
+
81
+ {{ snippet:pricing-table }}
82
+ ```
83
+
84
+ Snippets are injected verbatim as markdown. After all bindings resolve, the full document passes through the markdown renderer as usual.
85
+
86
+ ## Formatters
87
+
88
+ Apply with pipe syntax. Chaining composes left to right: `{{ key | round(0) | dollar }}`.
89
+
90
+ | Formatter | Input | Output | Notes |
91
+ | ---------- | ----------- | -------- | ------------------------------------------- |
92
+ | `dollar` | `"1500"` | `$1,500` | USD format; cents only if fractional |
93
+ | `number` | `"2500"` | `2,500` | Comma-separated |
94
+ | `percent` | `"0.15"` | `15%` | **Input must be a decimal ratio (0.0–1.0)** |
95
+ | `round(n)` | `"3.14159"` | `3.14` | Round to n decimal places |
96
+ | `upper` | `"hello"` | `HELLO` | Uppercase |
97
+ | `lower` | `"HELLO"` | `hello` | Lowercase |
98
+
99
+ **`percent` expects a decimal ratio.** `"0.15"` becomes `15%`. If your generator already computes integer percentages (like `"5"` for 5%), store them as pre-formatted strings (`"5%"`) and don't pipe through `percent` — that would yield `500%`.
100
+
101
+ All formatters are pure functions: string in, string out. The set is closed — adding a formatter requires a kitfly code change.
102
+
103
+ ## Error handling
104
+
105
+ Kitfly fails loud on binding errors. These are build errors, not warnings:
106
+
107
+ | Condition | Error message |
108
+ | ----------------------- | -------------------------------------------------------- |
109
+ | Data file not found | `data file not found: data/pricing.yaml` |
110
+ | Unresolved `{{ key }}` | `unresolved binding "key" in content/product/pricing.md` |
111
+ | Unknown snippet slot | `unknown snippet "name" in content/product/pricing.md` |
112
+ | Formatter parse failure | `dollar formatter: "hello" is not a number ...` |
113
+ | Unknown formatter | `unknown formatter "custom_fn" ...` |
114
+ | Path escapes kitsite | `data path escapes kitsite: ../secrets/data.yaml` |
115
+
116
+ ## Pre-build hooks
117
+
118
+ Declare generators in `site.yaml`:
119
+
120
+ ```yaml
121
+ prebuild:
122
+ - command: "bun run scripts/generate-pricing-data.ts"
123
+ watch: ["data/raw/pricing-input.json"]
124
+ - command: "bun run scripts/generate-team-data.ts"
125
+ ```
126
+
127
+ | Context | When hooks run |
128
+ | ---------------- | ----------------------------------------------------- |
129
+ | `bun run dev` | Once at startup, then again when watched files change |
130
+ | `bun run build` | Once before build starts |
131
+ | `bun run bundle` | Once before bundle starts |
132
+
133
+ Hooks run sequentially in declared order. A non-zero exit code halts the build with the hook's stderr as error context.
134
+
135
+ ### Watch flow in dev mode
136
+
137
+ ```
138
+ watched file changes → re-run matching hook → hook writes to data/ → data/ change triggers rebuild
139
+ ```
140
+
141
+ Content changes (edits to markdown) do NOT re-run hooks. Only watched file changes trigger hooks.
142
+
143
+ ## Schema validation (optional)
144
+
145
+ If `data/pricing.schema.json` exists alongside `data/pricing.yaml`, kitfly validates the data at build time. JSON Schema structural checks cover required fields, value types, and pattern constraints.
146
+
147
+ Missing schema files are not an error — validation is opt-in.
148
+
149
+ ## Generator best practices
150
+
151
+ These patterns emerged from real-world usage and apply to any non-trivial generator.
152
+
153
+ ### The template is the contract
154
+
155
+ Every `{{ snippet:X }}` in a markdown template means the generator must always emit a snippet named `X` in the data file, even if the content is empty. Kitfly treats missing snippets as build errors — by design.
156
+
157
+ If a section is conditionally relevant (e.g. an implementation-tiers table that only applies to multi-tier configurations), the generator should emit the snippet with empty string content when the section doesn't apply. The blank line in the rendered output is acceptable and avoids conditional logic in the template.
158
+
159
+ ```yaml
160
+ # Generator always emits this, even when single-tier
161
+ snippets:
162
+ - slot: implementation-tiers
163
+ content: ""
164
+ ```
165
+
166
+ ### Use JSON for generator output
167
+
168
+ Generators should write JSON data files. `JSON.stringify` is deterministic in every language, there's no quoting ambiguity, and multiline strings use unambiguous `\n` escapes.
169
+
170
+ Reserve YAML for hand-authored data files (team rosters, simple config) where human readability matters.
171
+
172
+ ### Separate raw input from kitfly data
173
+
174
+ Keep generator source files in `data/raw/` and kitfly data files in `data/`:
175
+
176
+ ```
177
+ data/
178
+ raw/
179
+ pricing-input.json ← generator reads this
180
+ team.csv ← generator reads this
181
+ pricing.json ← generator writes this (kitfly reads it)
182
+ team.json ← generator writes this (kitfly reads it)
183
+ ```
184
+
185
+ This prevents naming collisions and makes the data flow visible. The `prebuild:` hook `watch:` patterns point at `data/raw/` sources, and kitfly reads the generated files from `data/`.
186
+
187
+ ### Generator language
188
+
189
+ Generators can be written in any language. Kitfly runs them as shell commands. The motivating use case was a Python generator in an otherwise TypeScript/Bun ecosystem — pre-build hooks eliminate the manual step and integrate it into the dev/build pipeline regardless of language.
190
+
191
+ ## Interaction with content profiles
192
+
193
+ Data bindings and content profiles are independent features that compose naturally:
194
+
195
+ - Profile filtering runs first (after file collection, before rendering)
196
+ - Data binding resolution runs second (after reading markdown, before markdown rendering)
197
+ - A page excluded by profile filtering is never read, so its bindings are never resolved
198
+ - `KITFLY_PROFILE` is passed to pre-build hooks so generators can adapt output per profile
199
+
200
+ ## Backwards compatibility
201
+
202
+ - Pages without `data:` frontmatter: no binding resolution, literal `{{ }}` passes through unchanged
203
+ - Sites without `prebuild:` in site.yaml: no hooks run, zero overhead
204
+ - Sites without a `data/` directory: no binding resolution, zero overhead