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
@@ -33,3 +33,28 @@ plugins:
33
33
  assetSha256:
34
34
  js: "sha256:716059fbb0045af5305e552557368fe812569121986e0ce533f96753197c6ee1"
35
35
  css: "sha256:dae86214e5f3f628deb6fec3f7bd871fa419f0310993cde9e845392ac16d9ec1"
36
+ latex:
37
+ name: "LaTeX/KaTeX Math Rendering"
38
+ description: "LaTeX math rendering via KaTeX ($...$ inline, $$...$$ display, fenced math)"
39
+ version: "0.2.2"
40
+ contract: "1"
41
+ kitfly: ">=0.2.0 <1.0.0"
42
+ license: MIT
43
+ verified: true
44
+ assets:
45
+ js: "plugins-dist/latex.js"
46
+ assetSha256:
47
+ js: "sha256:5475e344dc424e3015c3a8b2fd8152101354fe2622d322a0fa4f73d1335b4232"
48
+ slides-charts-lite:
49
+ name: "Slides Charts Lite"
50
+ description: "Simple charts for Kitfly slides (bar, line, pie)"
51
+ version: "0.2.2"
52
+ contract: "1"
53
+ kitfly: ">=0.2.0 <1.0.0"
54
+ license: MIT
55
+ verified: true
56
+ modes: ["slides"]
57
+ assets:
58
+ js: "plugins-dist/slides-charts-lite.js"
59
+ assetSha256:
60
+ js: "sha256:ea1afbe48bc94c617af1ee2d53febcaa8d48e83253094a1cbef598f77ec15013"
@@ -22,6 +22,11 @@
22
22
  "description": "Root directory for content (relative to repo root)",
23
23
  "default": "."
24
24
  },
25
+ "dataroot": {
26
+ "type": "string",
27
+ "description": "Root directory for data binding files (relative to repo root)",
28
+ "default": "data"
29
+ },
25
30
  "title": {
26
31
  "type": "string",
27
32
  "description": "Site title shown in browser tab and header",
@@ -90,6 +95,10 @@
90
95
  "description": "Path to logo image (relative to site root)",
91
96
  "default": "assets/brand/logo.png"
92
97
  },
98
+ "logoDark": {
99
+ "type": "string",
100
+ "description": "Path to dark mode logo image (relative to site root). Shown when dark theme is active."
101
+ },
93
102
  "favicon": {
94
103
  "type": "string",
95
104
  "description": "Path to favicon image (relative to site root)",
@@ -191,6 +200,29 @@
191
200
  "type": "boolean",
192
201
  "description": "Show Built with Kitfly attribution",
193
202
  "default": true
203
+ },
204
+ "logo": {
205
+ "type": "string",
206
+ "description": "Path to footer logo image (relative to site root)"
207
+ },
208
+ "logoDark": {
209
+ "type": "string",
210
+ "description": "Path to dark mode footer logo image (relative to site root)"
211
+ },
212
+ "logoUrl": {
213
+ "type": "string",
214
+ "description": "Make footer logo a clickable link to this URL"
215
+ },
216
+ "logoAlt": {
217
+ "type": "string",
218
+ "description": "Alt text for footer logo (defaults to copyright text if set, brand name otherwise)"
219
+ },
220
+ "logoHeight": {
221
+ "type": "integer",
222
+ "description": "Max height of footer logo in pixels",
223
+ "minimum": 10,
224
+ "maximum": 40,
225
+ "default": 20
194
226
  }
195
227
  },
196
228
  "additionalProperties": false
@@ -213,6 +245,30 @@
213
245
  }
214
246
  },
215
247
  "additionalProperties": false
248
+ },
249
+ "prebuild": {
250
+ "type": "array",
251
+ "description": "Commands to run before dev/build/bundle",
252
+ "items": {
253
+ "type": "object",
254
+ "required": [
255
+ "command"
256
+ ],
257
+ "properties": {
258
+ "command": {
259
+ "type": "string",
260
+ "description": "Shell command to execute"
261
+ },
262
+ "watch": {
263
+ "type": "array",
264
+ "description": "File patterns that re-run this hook in dev mode",
265
+ "items": {
266
+ "type": "string"
267
+ }
268
+ }
269
+ },
270
+ "additionalProperties": false
271
+ }
216
272
  }
217
273
  },
218
274
  "additionalProperties": false
package/scripts/build.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Options:
7
7
  * -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: dist]
8
+ * --profile <name> Active content profile [env: KITFLY_PROFILE]
8
9
  * --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
9
10
  * --no-raw Don't include raw markdown files
10
11
  * --help Show help message
@@ -20,9 +21,10 @@ import { loadPluginInjections, type PluginInjections } from "../src/plugin-loade
20
21
  import {
21
22
  buildBreadcrumbsStatic,
22
23
  buildFooter,
24
+ buildLogoImgHtml,
23
25
  buildNavStatic,
24
26
  buildPageMeta,
25
- buildSlideNav,
27
+ buildSlideNavHierarchical,
26
28
  buildToc,
27
29
  type ContentFile,
28
30
  collectFiles,
@@ -35,18 +37,24 @@ import {
35
37
  escapeHtml,
36
38
  // File utilities
37
39
  exists,
40
+ filterByProfile,
38
41
  filterUnknownSlidesVisualsTypeDiagnostics,
39
42
  // Provenance
40
43
  generateProvenance,
44
+ loadDataBindings,
41
45
  // YAML/Config parsing
42
46
  loadSiteConfig,
47
+ mergeFrontmatterWithBody,
43
48
  type Provenance,
49
+ pagePathForData,
44
50
  // Markdown utilities
45
51
  parseFrontmatter,
46
52
  parseYaml,
53
+ resolveBindings,
47
54
  resolveStylesPath,
48
55
  resolveTemplatePath,
49
56
  rewriteRelativeAssetUrls,
57
+ runPrebuildHooks,
50
58
  // Types
51
59
  type SiteConfig,
52
60
  slugify,
@@ -60,6 +68,41 @@ const DEFAULT_OUT = "dist";
60
68
 
61
69
  let ROOT = process.cwd();
62
70
  let OUT_DIR = DEFAULT_OUT;
71
+ let ACTIVE_PROFILE: string | undefined;
72
+
73
+ async function applyDataBindingsToMarkdown(
74
+ rawMarkdown: string,
75
+ filePath: string,
76
+ config: SiteConfig,
77
+ ): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
78
+ const parsed = parseFrontmatter(rawMarkdown);
79
+ const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
80
+ if (!dataRef) return parsed;
81
+
82
+ const pagePath = pagePathForData(ROOT, config.docroot, filePath);
83
+ const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
84
+ return {
85
+ frontmatter: parsed.frontmatter,
86
+ body: resolveBindings(parsed.body, bindings, pagePath),
87
+ };
88
+ }
89
+
90
+ async function applyDataBindingsForSlides(
91
+ rawMarkdown: string,
92
+ filePath: string,
93
+ config: SiteConfig,
94
+ ): Promise<string> {
95
+ const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
96
+ return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
97
+ }
98
+
99
+ function normalizeMsysPath(p: string): string {
100
+ // Git Bash / MSYS-style paths: /c/Users/... -> C:\Users\...
101
+ if (process.platform !== "win32") return p;
102
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
103
+ if (!m) return p;
104
+ return `${m[1].toUpperCase()}:\\${m[2].replaceAll("/", "\\")}`;
105
+ }
63
106
 
64
107
  // ---------------------------------------------------------------------------
65
108
  // CLI argument parsing
@@ -69,6 +112,7 @@ interface ParsedArgs {
69
112
  folder?: string;
70
113
  out?: string;
71
114
  raw?: boolean;
115
+ profile?: string;
72
116
  }
73
117
 
74
118
  function parseArgs(argv: string[]): ParsedArgs {
@@ -80,6 +124,9 @@ function parseArgs(argv: string[]): ParsedArgs {
80
124
  if ((arg === "--out" || arg === "-o") && next && !next.startsWith("-")) {
81
125
  result.out = next;
82
126
  i++;
127
+ } else if (arg === "--profile" && next && !next.startsWith("-")) {
128
+ result.profile = next;
129
+ i++;
83
130
  } else if (arg === "--raw") {
84
131
  result.raw = true;
85
132
  } else if (arg === "--no-raw") {
@@ -91,12 +138,13 @@ function parseArgs(argv: string[]): ParsedArgs {
91
138
  return result;
92
139
  }
93
140
 
94
- function getConfig(): { folder?: string; out: string; raw: boolean } {
141
+ function getConfig(): { folder?: string; out: string; raw: boolean; profile?: string } {
95
142
  const args = parseArgs(process.argv.slice(2));
96
143
  return {
97
144
  folder: args.folder,
98
145
  out: args.out ?? envString("KITFLY_BUILD_OUT", DEFAULT_OUT),
99
146
  raw: args.raw ?? envBool("KITFLY_BUILD_RAW", true),
147
+ profile: args.profile ?? process.env.KITFLY_PROFILE,
100
148
  };
101
149
  }
102
150
 
@@ -189,7 +237,7 @@ async function renderFile(
189
237
  }
190
238
  htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
191
239
  } else {
192
- const { frontmatter, body } = parseFrontmatter(content);
240
+ const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
193
241
  if (frontmatter.title) {
194
242
  title = frontmatter.title as string;
195
243
  }
@@ -199,7 +247,7 @@ async function renderFile(
199
247
 
200
248
  const pathPrefix = computePathPrefix(urlKey);
201
249
  const nav = buildNavStatic(files, urlKey, config, pathPrefix);
202
- const footer = buildFooter(provenance, config);
250
+ const footer = buildFooter(provenance, config, pathPrefix);
203
251
  const breadcrumbs = buildBreadcrumbsStatic(urlKey, pathPrefix, files, config);
204
252
  const toc = buildToc(htmlContent);
205
253
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
@@ -207,32 +255,50 @@ async function renderFile(
207
255
  const prismUrls = getPrismUrls(theme);
208
256
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
209
257
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
258
+ const mobileLogoHtml = buildLogoImgHtml({
259
+ logo: config.brand.logo || "assets/brand/logo.png",
260
+ logoDark: config.brand.logoDark,
261
+ alt: config.brand.name,
262
+ className: `logo-img ${logoClass}`,
263
+ pathPrefix,
264
+ onerrorFallback: true,
265
+ });
266
+ const sidebarLogoHtml = buildLogoImgHtml({
267
+ logo: config.brand.logo || "assets/brand/logo.png",
268
+ logoDark: config.brand.logoDark,
269
+ alt: config.brand.name,
270
+ className: "logo-img",
271
+ pathPrefix,
272
+ onerrorFallback: true,
273
+ });
210
274
 
211
275
  return template
212
276
  .replace("{{BODY_CLASS}}", "mode-docs")
213
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
214
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
215
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
216
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
217
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
218
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
219
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
220
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
221
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
222
- .replace("{{TITLE}}", title)
223
- .replace("{{VERSION}}", uiVersion)
224
- .replace("{{BRANCH}}", provenance.gitBranch)
225
- .replace("{{BREADCRUMBS}}", breadcrumbs)
226
- .replace("{{PAGE_META}}", pageMeta)
227
- .replace("{{NAV}}", nav)
228
- .replace("{{CONTENT}}", htmlContent)
229
- .replace("{{TOC}}", toc)
230
- .replace("{{FOOTER}}", footer)
231
- .replace("{{THEME_CSS}}", themeCSS)
232
- .replace("{{PLUGIN_HEAD}}", plugins.head)
233
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
234
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
235
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
277
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
278
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
279
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
280
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
281
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
282
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
283
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
284
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
285
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
286
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
287
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
288
+ .replace("{{TITLE}}", () => title)
289
+ .replace("{{VERSION}}", () => uiVersion)
290
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
291
+ .replace("{{BREADCRUMBS}}", () => breadcrumbs)
292
+ .replace("{{PAGE_META}}", () => pageMeta)
293
+ .replace("{{NAV}}", () => nav)
294
+ .replace("{{CONTENT}}", () => htmlContent)
295
+ .replace("{{TOC}}", () => toc)
296
+ .replace("{{FOOTER}}", () => footer)
297
+ .replace("{{THEME_CSS}}", () => themeCSS)
298
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
299
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
300
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
301
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
236
302
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
237
303
  }
238
304
 
@@ -274,32 +340,50 @@ sections:
274
340
  const pathPrefix = "./";
275
341
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
276
342
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
343
+ const mobileLogoHtml = buildLogoImgHtml({
344
+ logo: config.brand.logo || "assets/brand/logo.png",
345
+ logoDark: config.brand.logoDark,
346
+ alt: config.brand.name,
347
+ className: `logo-img ${logoClass}`,
348
+ pathPrefix,
349
+ onerrorFallback: true,
350
+ });
351
+ const sidebarLogoHtml = buildLogoImgHtml({
352
+ logo: config.brand.logo || "assets/brand/logo.png",
353
+ logoDark: config.brand.logoDark,
354
+ alt: config.brand.name,
355
+ className: "logo-img",
356
+ pathPrefix,
357
+ onerrorFallback: true,
358
+ });
277
359
 
278
360
  return template
279
361
  .replace("{{BODY_CLASS}}", "mode-docs")
280
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
281
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
282
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
283
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
284
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
285
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
286
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
287
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
288
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
362
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
363
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
364
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
365
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
366
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
367
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
368
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
369
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
370
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
371
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
372
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
289
373
  .replace("{{TITLE}}", "Getting Started")
290
- .replace("{{VERSION}}", uiVersion)
291
- .replace("{{BRANCH}}", provenance.gitBranch)
374
+ .replace("{{VERSION}}", () => uiVersion)
375
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
292
376
  .replace("{{BREADCRUMBS}}", "")
293
377
  .replace("{{PAGE_META}}", "")
294
378
  .replace("{{NAV}}", "<ul></ul>")
295
- .replace("{{CONTENT}}", htmlContent)
379
+ .replace("{{CONTENT}}", () => htmlContent)
296
380
  .replace("{{TOC}}", "")
297
- .replace("{{FOOTER}}", buildFooter(provenance, config))
298
- .replace("{{THEME_CSS}}", themeCSS)
299
- .replace("{{PLUGIN_HEAD}}", plugins.head)
300
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
301
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
302
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
381
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
382
+ .replace("{{THEME_CSS}}", () => themeCSS)
383
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
384
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
385
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
386
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
303
387
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
304
388
  }
305
389
 
@@ -313,7 +397,9 @@ async function renderSlidesIndex(
313
397
  ): Promise<string> {
314
398
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
315
399
  const pathPrefix = "./";
316
- const slides = await collectSlides(files);
400
+ const slides = await collectSlides(files, {
401
+ markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
402
+ });
317
403
  let validateFences = false;
318
404
  try {
319
405
  const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
@@ -376,38 +462,56 @@ async function renderSlidesIndex(
376
462
  </div>
377
463
  </div>`;
378
464
 
379
- const nav = buildSlideNav(slides, config, "slide-1");
465
+ const nav = buildSlideNavHierarchical(slides, config, "slide-1");
380
466
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
381
467
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
382
468
  const themeCSS = generateThemeCSS(theme);
383
469
  const prismUrls = getPrismUrls(theme);
384
470
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
471
+ const mobileLogoHtml = buildLogoImgHtml({
472
+ logo: config.brand.logo || "assets/brand/logo.png",
473
+ logoDark: config.brand.logoDark,
474
+ alt: config.brand.name,
475
+ className: `logo-img ${logoClass}`,
476
+ pathPrefix,
477
+ onerrorFallback: true,
478
+ });
479
+ const sidebarLogoHtml = buildLogoImgHtml({
480
+ logo: config.brand.logo || "assets/brand/logo.png",
481
+ logoDark: config.brand.logoDark,
482
+ alt: config.brand.name,
483
+ className: "logo-img",
484
+ pathPrefix,
485
+ onerrorFallback: true,
486
+ });
385
487
 
386
488
  return template
387
489
  .replace("{{BODY_CLASS}}", "mode-slides")
388
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
389
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
390
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
391
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
392
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
393
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
394
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
395
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
396
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
397
- .replace("{{TITLE}}", config.title)
398
- .replace("{{VERSION}}", uiVersion)
399
- .replace("{{BRANCH}}", provenance.gitBranch)
490
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
491
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
492
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
493
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
494
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
495
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
496
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
497
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
498
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
499
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
500
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
501
+ .replace("{{TITLE}}", () => config.title)
502
+ .replace("{{VERSION}}", () => uiVersion)
503
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
400
504
  .replace("{{BREADCRUMBS}}", "")
401
505
  .replace("{{PAGE_META}}", "")
402
- .replace("{{NAV}}", nav)
403
- .replace("{{CONTENT}}", htmlContent)
506
+ .replace("{{NAV}}", () => nav)
507
+ .replace("{{CONTENT}}", () => htmlContent)
404
508
  .replace("{{TOC}}", "")
405
- .replace("{{FOOTER}}", buildFooter(provenance, config))
406
- .replace("{{THEME_CSS}}", themeCSS)
407
- .replace("{{PLUGIN_HEAD}}", plugins.head)
408
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
409
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
410
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
509
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
510
+ .replace("{{THEME_CSS}}", () => themeCSS)
511
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
512
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
513
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
514
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
411
515
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
412
516
  }
413
517
 
@@ -416,6 +520,7 @@ export interface BuildOptions {
416
520
  folder?: string;
417
521
  out?: string;
418
522
  raw?: boolean; // Include raw markdown files (default: true)
523
+ profile?: string;
419
524
  }
420
525
 
421
526
  let INCLUDE_RAW = true;
@@ -430,18 +535,29 @@ export async function build(options: BuildOptions = {}) {
430
535
  if (options.raw === false) {
431
536
  INCLUDE_RAW = false;
432
537
  }
538
+ ACTIVE_PROFILE = options.profile;
433
539
  await buildSite();
434
540
  }
435
541
 
436
542
  // Rename internal function
437
543
  async function buildSite() {
438
- const DIST = join(ROOT, OUT_DIR);
544
+ const DIST = resolve(ROOT, normalizeMsysPath(OUT_DIR));
439
545
 
440
546
  console.log("Building site...\n");
441
547
 
442
548
  // Load configuration
443
549
  const config = await loadSiteConfig(ROOT);
444
550
  console.log(` ✓ Loaded config: "${config.title}" (${config.sections.length} sections)`);
551
+ if (config.prebuild?.length) {
552
+ await runPrebuildHooks(
553
+ config.prebuild,
554
+ ROOT,
555
+ "build",
556
+ ACTIVE_PROFILE,
557
+ config.dataroot || "data",
558
+ );
559
+ console.log(` ✓ prebuild hooks (${config.prebuild.length})`);
560
+ }
445
561
 
446
562
  // Load theme
447
563
  const theme = await loadTheme(ROOT);
@@ -499,7 +615,11 @@ async function buildSite() {
499
615
  }
500
616
 
501
617
  // Collect and render all files
502
- const files = await collectFiles(ROOT, config);
618
+ const files = await filterByProfile(
619
+ await collectFiles(ROOT, config),
620
+ ACTIVE_PROFILE,
621
+ config.profiles,
622
+ );
503
623
 
504
624
  if (files.length === 0) {
505
625
  // No content - render Getting Started page
@@ -733,6 +853,7 @@ Usage: bun run build [folder] [options]
733
853
 
734
854
  Options:
735
855
  -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: ${DEFAULT_OUT}]
856
+ --profile <name> Active content profile [env: KITFLY_PROFILE]
736
857
  --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
737
858
  --no-raw Don't include raw markdown files
738
859
  --help Show this help message
@@ -752,5 +873,6 @@ Examples:
752
873
  folder: cfg.folder,
753
874
  out: cfg.out,
754
875
  raw: cfg.raw,
876
+ profile: cfg.profile,
755
877
  }).catch(console.error);
756
878
  }