@stackbox/cms 0.0.3 → 0.0.5

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 (117) hide show
  1. package/AGENTS.md +87 -0
  2. package/README.md +57 -17
  3. package/dist/blocks.d.ts +18 -0
  4. package/dist/blocks.d.ts.map +1 -0
  5. package/dist/blocks.js +25 -0
  6. package/dist/blocks.js.map +1 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +12 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/link.d.ts +10 -0
  12. package/dist/link.d.ts.map +1 -0
  13. package/dist/link.js +128 -0
  14. package/dist/link.js.map +1 -0
  15. package/dist/modules/blog/module.d.ts +20 -0
  16. package/dist/modules/blog/module.d.ts.map +1 -0
  17. package/dist/modules/blog/module.js +52 -0
  18. package/dist/modules/blog/module.js.map +1 -0
  19. package/dist/modules/blog/posts.d.ts +15 -0
  20. package/dist/modules/blog/posts.d.ts.map +1 -0
  21. package/dist/modules/blog/posts.js +46 -0
  22. package/dist/modules/blog/posts.js.map +1 -0
  23. package/dist/modules.d.ts +18 -0
  24. package/dist/modules.d.ts.map +1 -0
  25. package/dist/modules.js +25 -0
  26. package/dist/modules.js.map +1 -0
  27. package/dist/pages.d.ts +55 -0
  28. package/dist/pages.d.ts.map +1 -0
  29. package/dist/pages.js +85 -0
  30. package/dist/pages.js.map +1 -0
  31. package/dist/plugins/blog/index.d.ts +20 -0
  32. package/dist/plugins/blog/index.d.ts.map +1 -0
  33. package/dist/plugins/blog/index.js +52 -0
  34. package/dist/plugins/blog/index.js.map +1 -0
  35. package/dist/plugins/blog/module.d.ts +20 -0
  36. package/dist/plugins/blog/module.d.ts.map +1 -0
  37. package/dist/plugins/blog/module.js +52 -0
  38. package/dist/plugins/blog/module.js.map +1 -0
  39. package/dist/plugins/blog/plugin.d.ts +20 -0
  40. package/dist/plugins/blog/plugin.d.ts.map +1 -0
  41. package/dist/plugins/blog/plugin.js +52 -0
  42. package/dist/plugins/blog/plugin.js.map +1 -0
  43. package/dist/plugins/blog/posts.d.ts +15 -0
  44. package/dist/plugins/blog/posts.d.ts.map +1 -0
  45. package/dist/plugins/blog/posts.js +46 -0
  46. package/dist/plugins/blog/posts.js.map +1 -0
  47. package/dist/plugins/random-quote/index.d.ts +12 -0
  48. package/dist/plugins/random-quote/index.d.ts.map +1 -0
  49. package/dist/plugins/random-quote/index.js +21 -0
  50. package/dist/plugins/random-quote/index.js.map +1 -0
  51. package/dist/plugins/random-quote/module.d.ts +9 -0
  52. package/dist/plugins/random-quote/module.d.ts.map +1 -0
  53. package/dist/plugins/random-quote/module.js +31 -0
  54. package/dist/plugins/random-quote/module.js.map +1 -0
  55. package/dist/plugins/random-quote/plugin.d.ts +8 -0
  56. package/dist/plugins/random-quote/plugin.d.ts.map +1 -0
  57. package/dist/plugins/random-quote/plugin.js +31 -0
  58. package/dist/plugins/random-quote/plugin.js.map +1 -0
  59. package/dist/plugins/random-quote/public_assets/quotes.json +230 -0
  60. package/dist/plugins/random-quote/quotes.json +230 -0
  61. package/dist/public/_sb/plugins/random-quote/quotes.json +230 -0
  62. package/dist/render-head.d.ts +7 -0
  63. package/dist/render-head.d.ts.map +1 -0
  64. package/dist/render-head.js +27 -0
  65. package/dist/render-head.js.map +1 -0
  66. package/dist/render-page.d.ts +6 -0
  67. package/dist/render-page.d.ts.map +1 -0
  68. package/dist/render-page.js +30 -0
  69. package/dist/render-page.js.map +1 -0
  70. package/dist/routing.d.ts +3 -0
  71. package/dist/routing.d.ts.map +1 -0
  72. package/dist/routing.js +11 -0
  73. package/dist/routing.js.map +1 -0
  74. package/dist/site.d.ts +21 -0
  75. package/dist/site.d.ts.map +1 -0
  76. package/dist/site.js +102 -0
  77. package/dist/site.js.map +1 -0
  78. package/dist/slot-content.d.ts +30 -0
  79. package/dist/slot-content.d.ts.map +1 -0
  80. package/dist/slot-content.js +34 -0
  81. package/dist/slot-content.js.map +1 -0
  82. package/dist/slot-handle.d.ts +32 -0
  83. package/dist/slot-handle.d.ts.map +1 -0
  84. package/dist/slot-handle.js +68 -0
  85. package/dist/slot-handle.js.map +1 -0
  86. package/dist/stackbox/context.d.ts +52 -0
  87. package/dist/stackbox/context.d.ts.map +1 -0
  88. package/dist/stackbox/context.js +165 -0
  89. package/dist/stackbox/context.js.map +1 -0
  90. package/dist/templates.d.ts +63 -0
  91. package/dist/templates.d.ts.map +1 -0
  92. package/dist/templates.js +106 -0
  93. package/dist/templates.js.map +1 -0
  94. package/package.json +19 -6
  95. package/src/plugins/blog/AGENTS.md +131 -0
  96. package/src/plugins/random-quote/AGENTS.md +79 -0
  97. package/src/plugins/random-quote/index.ts +33 -0
  98. package/src/plugins/random-quote/public_assets/quotes.json +230 -0
  99. package/.claude/settings.local.json +0 -8
  100. package/src/index.ts +0 -67
  101. package/src/modules.ts +0 -58
  102. package/src/pages.ts +0 -198
  103. package/src/render-head.ts +0 -32
  104. package/src/render-page.ts +0 -51
  105. package/src/routing.ts +0 -14
  106. package/src/site.ts +0 -153
  107. package/src/slot-content.ts +0 -77
  108. package/src/slot-handle.ts +0 -131
  109. package/src/stackbox/context.ts +0 -257
  110. package/src/templates.ts +0 -230
  111. package/test/async-render.test.ts +0 -103
  112. package/test/blog.test.ts +0 -93
  113. package/test/fetch-routing.test.ts +0 -76
  114. package/test/stackbox-context.test.ts +0 -44
  115. package/tsconfig.json +0 -19
  116. /package/src/{modules/blog/module.ts → plugins/blog/index.ts} +0 -0
  117. /package/src/{modules → plugins}/blog/posts.ts +0 -0
package/AGENTS.md ADDED
@@ -0,0 +1,87 @@
1
+ # @stackbox/cms — agent instructions
2
+
3
+ Stackbox is a code-first CMS that assembles TypeScript into a **fetch-handler-compatible server**. Use these conventions when building or extending a site.
4
+
5
+ ## Site conventions
6
+
7
+ | Primitive | Factory | Purpose |
8
+ | --- | --- | --- |
9
+ | Site config | `createSiteConfig(config)` | Definition-time settings shared by templates and pages |
10
+ | Site | `createSite(siteConfig, { pages })` | Runtime router with `fetch(request, env)` |
11
+ | Template | `createTemplate({ siteConfig, slots, render })` | Reusable layout with named slots |
12
+ | Page | `createPage(template, { path, title, slots })` | A single URL built from a template + slot content |
13
+ | Block | `createBlock({ name, render })` | A content block placed into a slot at request time |
14
+
15
+ **Project layout:**
16
+
17
+ ```
18
+ site.config.ts # createSiteConfig
19
+ server.ts # createSite(...) — default export is the fetch handler
20
+ templates/ # shared createTemplate() layouts
21
+ pages/ # createPage() per route or feature
22
+ content/ # markdown or other bundle-time content (per feature)
23
+ ```
24
+
25
+ The default export from `server.ts` implements `fetch(request, env)` and returns a `Response`. It works on Cloudflare Workers, Bun, Deno, and any runtime that speaks the fetch-handler pattern.
26
+
27
+ ## Plugins vs blocks
28
+
29
+ - **Plugin** — a packaged use-case under `@stackbox/cms/plugins/<name>` (e.g. blog). May export factories, content objects, blocks, types, and helpers.
30
+ - **Block** — a core primitive via `createBlock()`. Renders HTML into a template slot. Usable inside or outside plugins.
31
+
32
+ Slot content is `Block | string`. Plugin content arrays can interleave blocks and HTML strings.
33
+
34
+ ## Plugin catalog
35
+
36
+ Before implementing a use-case (blog, newsletter, docs, …), check this table. If the user's request matches a row, **read only that plugin's AGENTS.md** and follow it. Do not invent a parallel implementation.
37
+
38
+ | Plugin | Import | Read when the user mentions | Instructions |
39
+ | --- | --- | --- | --- |
40
+ | blog | `@stackbox/cms/plugins/blog` | blog, posts, articles, journal, markdown posts, blog listing, blog page | `src/plugins/blog/AGENTS.md` |
41
+ | random-quote | `@stackbox/cms/plugins/random-quote` | random quote, quote of the day, inspirational quote, sidebar quote | `src/plugins/random-quote/AGENTS.md` |
42
+
43
+ When adding a new plugin to this package, add a row here and create `src/plugins/<name>/AGENTS.md` using the same heading structure as the blog plugin. Document all export kinds (factory, types, blocks, helpers).
44
+
45
+ ## Plugin public assets
46
+
47
+ Runtime files that must be **publicly available** (images, CSS, fonts, JSON served over HTTP) go in:
48
+
49
+ ```
50
+ src/plugins/<name>/public_assets/
51
+ ```
52
+
53
+ **Opt-in:** only plugins with a `public_assets/` folder get copied at build time. Do not edit the copy script when adding files.
54
+
55
+ Build copies each `public_assets/` folder to:
56
+
57
+ - `dist/public/_sb/plugins/<name>/` — served at `/_sb/plugins/<name>/...`
58
+ - `dist/plugins/<name>/public_assets/` — so `import "./public_assets/..." with { type: "json" }` works in compiled JS
59
+
60
+ Use `pluginAssetPath()` from `@stackbox/cms/link` for `<img src>`, `<link href>`, etc. Do not put site files under `/_sb/` — that prefix is reserved for Stackbox plugin assets.
61
+
62
+ ```ts
63
+ import { pluginAssetPath } from "@stackbox/cms/link";
64
+
65
+ pluginAssetPath("random-quote", "photo.jpg");
66
+ // → "/_sb/plugins/random-quote/photo.jpg"
67
+ ```
68
+
69
+ For data bundled into JS at build time, import from `./public_assets/...` in the plugin's `index.ts`.
70
+
71
+ ## For site agents
72
+
73
+ If you are working in a **consumer site repo** (not this package), read this file from the installed package:
74
+
75
+ ```
76
+ node_modules/@stackbox/cms/AGENTS.md
77
+ ```
78
+
79
+ Recommended snippet for the site's own `AGENTS.md`:
80
+
81
+ ```md
82
+ This site uses @stackbox/cms. Before adding features, read
83
+ `node_modules/@stackbox/cms/AGENTS.md` and follow its plugin catalog.
84
+ Do not reimplement bundled plugins.
85
+ ```
86
+
87
+ When a request matches a catalog row, open **only** that plugin's instructions file (e.g. `node_modules/@stackbox/cms/src/plugins/blog/AGENTS.md`).
package/README.md CHANGED
@@ -1,12 +1,22 @@
1
1
  # @stackbox/cms
2
2
 
3
- A small, code-first CMS engine for building dynamic [Cloudflare Worker](https://developers.cloudflare.com/workers/) sites. Stackbox is designed to be driven by AI: pages, templates, and content modules are plain TypeScript files with typed, composable APIs, so an agent can author and assemble a site without a database, admin UI, or hand-written backend.
3
+ A small, code-first CMS engine for building dynamic sites behind a standard **fetch handler**. Stackbox is designed to be driven by AI: pages, templates, blocks, and plugins are plain TypeScript files with typed, composable APIs, so an agent can author and assemble a site without a database, admin UI, or hand-written backend.
4
4
 
5
- Pages are rendered on each request inside a Cloudflare Worker, so content, templates, and modules can be fully dynamic — driven by request data, bindings (KV, D1, R2), and async data fetching.
5
+ `createSite()` returns an object with a `fetch(request, env)` method — the same shape used by Cloudflare Workers, Bun, Deno, and other runtimes that serve HTTP via the Fetch API:
6
+
7
+ ```ts
8
+ export default {
9
+ fetch(req: Request): Response | Promise<Response> {
10
+ return new Response(...);
11
+ },
12
+ };
13
+ ```
14
+
15
+ Pages are rendered on each request, so content, templates, blocks, and plugins can be fully dynamic — driven by request data, environment bindings, and async data fetching.
6
16
 
7
17
  ## Why this exists
8
18
 
9
- Traditional CMSes assume a human clicking around an admin panel. Stackbox inverts that: a site is TypeScript modules assembled into a Worker. Every primitive (`createSiteConfig`, `createSite`, `createTemplate`, `createPage`, `createModule`) is a typed factory suited for AI to generate, edit, and validate site content as code — and the same files render dynamically on Cloudflare Workers at request time.
19
+ Traditional CMSes assume a human clicking around an admin panel. Stackbox inverts that: a site is TypeScript assembled into a fetch handler. Every primitive (`createSiteConfig`, `createSite`, `createTemplate`, `createPage`, `createBlock`) is a typed factory suited for AI to generate, edit, and validate site content as code — and the same files render dynamically at request time.
10
20
 
11
21
  ## Requirements
12
22
 
@@ -23,19 +33,21 @@ npm install @stackbox/cms
23
33
  | Primitive | Factory | Purpose |
24
34
  | --- | --- | --- |
25
35
  | **Site config** | `createSiteConfig(config)` | Definition-time settings shared by templates and pages. |
26
- | **Site** | `createSite(siteConfig, { pages }` | Runtime router with `fetch(request, env)` for Cloudflare Workers. |
27
- | **Template** | `createTemplate({ siteConfig, slots, render }` | A reusable page layout that declares named **slots**. |
28
- | **Page** | `createPage(template, { path, title, slots }` | A single URL, built by filling a template's slots with content. |
29
- | **Module** | `createModule({ name, render }` | A self-contained content block placed into a slot. |
36
+ | **Site** | `createSite(siteConfig, { pages })` | Runtime router with `fetch(request, env)` — a fetch-handler-compatible server. |
37
+ | **Template** | `createTemplate({ siteConfig, slots, render })` | A reusable page layout that declares named **slots**. |
38
+ | **Page** | `createPage(template, { path, title, slots })` | A single URL, built by filling a template's slots with content. |
39
+ | **Block** | `createBlock({ name, render })` | A self-contained content block placed into a slot at request time. |
30
40
 
31
- **Slots** are named regions in a template. Page content — strings, HTML, or modules — is dropped into slots, and the engine resolves and renders everything (including `async` modules, concurrently) to a single HTML string.
41
+ **Plugins vs blocks:** **Plugins** package whole features (blog, newsletter) under `@stackbox/cms/plugins/<name>` — they may export factories, content objects, blocks, types, and helpers. **Blocks** are the core slot primitive via `createBlock()`; plugins can ship blocks alongside other exports.
42
+
43
+ **Slots** are named regions in a template. Page content — strings, HTML, or blocks — is dropped into slots, and the engine resolves and renders everything (including async blocks, concurrently) to a single HTML string.
32
44
 
33
45
  ## Project layout
34
46
 
35
47
  ```
36
- my-worker/
48
+ my-site/
37
49
  site.config.ts # createSiteConfig({ name, url, ... })
38
- worker.ts # createSite(siteConfig, { pages }) — default export for Cloudflare
50
+ server.ts # createSite(...) — default export is your fetch handler
39
51
  templates/
40
52
  site-template.ts # shared createTemplate() layouts
41
53
  pages/
@@ -87,7 +99,7 @@ const homePage = createPage(siteTemplate, {
87
99
  export default homePage;
88
100
  ```
89
101
 
90
- `worker.ts`:
102
+ `server.ts`:
91
103
 
92
104
  ```ts
93
105
  import { createSite } from "@stackbox/cms";
@@ -100,9 +112,13 @@ export default createSite(siteConfig, {
100
112
  });
101
113
  ```
102
114
 
103
- Deploy with [`wrangler`](https://developers.cloudflare.com/workers/wrangler/). The default export's `fetch(request, env)` handles each request.
115
+ The default export implements `fetch(request, env)` and returns a `Response` — drop it into any runtime that speaks the fetch-handler pattern. For example:
116
+
117
+ - **[Cloudflare Workers](https://developers.cloudflare.com/workers/)** — deploy with [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) (often as `worker.ts`)
118
+ - **[Bun](https://bun.sh/docs/api/http#fetch-handler)** — `Bun.serve({ fetch: site.fetch })`
119
+ - **[Deno](https://docs.deno.com/runtime/fundamentals/http_server/)** — `Deno.serve(site.fetch)`
104
120
 
105
- ## Blog module
121
+ ## Blog plugin
106
122
 
107
123
  `createBlog()` loads markdown at bundle time and returns **content objects** you wire into your own pages with `createPage()` — so you control templates, slots, and any extra content alongside blog output.
108
124
 
@@ -110,7 +126,7 @@ Deploy with [`wrangler`](https://developers.cloudflare.com/workers/wrangler/). T
110
126
  // pages/blog.ts
111
127
  import { join } from "node:path";
112
128
  import { createPage } from "@stackbox/cms";
113
- import { createBlog } from "@stackbox/cms/modules/blog";
129
+ import { createBlog } from "@stackbox/cms/plugins/blog";
114
130
  import { siteTemplate } from "../templates/site-template";
115
131
 
116
132
  const blog = createBlog({
@@ -140,7 +156,7 @@ export const blogPostPages = blog.posts.map((post) =>
140
156
  ```
141
157
 
142
158
  ```ts
143
- // worker.ts
159
+ // server.ts
144
160
  import { blogListingPages, blogPostPages } from "./pages/blog";
145
161
 
146
162
  export default createSite(siteConfig, {
@@ -148,10 +164,34 @@ export default createSite(siteConfig, {
148
164
  });
149
165
  ```
150
166
 
151
- ## Bundled modules
167
+ ## Bundled plugins
168
+
169
+ **Blog** — content objects wired into pages:
170
+
171
+ ```ts
172
+ import { createBlog } from "@stackbox/cms/plugins/blog";
173
+ ```
174
+
175
+ **Random quote** — block-only plugin (drop into any slot):
152
176
 
153
177
  ```ts
154
- import { createBlog } from "@stackbox/cms/modules/blog";
178
+ import { randomQuoteBlock } from "@stackbox/cms/plugins/random-quote";
179
+ import myQuotes from "../content/quotes.json" with { type: "json" };
180
+
181
+ slots: { sidebar: [randomQuoteBlock()] } // bundled quotes
182
+ slots: { sidebar: [randomQuoteBlock({ quotes: myQuotes })] } // your own
183
+ ```
184
+
185
+ ## AI agents
186
+
187
+ Bundled plugins include agent playbooks. See [`AGENTS.md`](AGENTS.md) for site conventions and a plugin catalog. When a user asks for a feature (e.g. "add a blog"), read **only** the matching plugin's `AGENTS.md` — do not load every plugin file.
188
+
189
+ If you are building a site that uses this package, add this to your project's `AGENTS.md`:
190
+
191
+ ```md
192
+ This site uses @stackbox/cms. Before adding features, read
193
+ `node_modules/@stackbox/cms/AGENTS.md` and follow its plugin catalog.
194
+ Do not reimplement bundled plugins.
155
195
  ```
156
196
 
157
197
  ## Development
@@ -0,0 +1,18 @@
1
+ import type { RenderContext } from "./pages.js";
2
+ import type { HSHtml } from "@hyperspan/html";
3
+ export type BlockRenderResult = HSHtml | Promise<HSHtml>;
4
+ export type Block = {
5
+ readonly __kind: "block";
6
+ readonly name: string;
7
+ render(ctx: RenderContext): BlockRenderResult;
8
+ };
9
+ type BlockDef<TOptions = undefined> = {
10
+ name: string;
11
+ render(options: TOptions, ctx?: RenderContext): BlockRenderResult;
12
+ };
13
+ export type BlockOptionsOf<F> = F extends (options?: infer O) => unknown ? [O] extends [undefined] ? undefined : O : never;
14
+ export type BlockFactory<TOptions = undefined> = (options?: TOptions) => Block;
15
+ export declare function createBlock<TOptions = undefined>(def: BlockDef<TOptions>): BlockFactory<TOptions>;
16
+ export declare function isBlock(value: unknown): value is Block;
17
+ export {};
18
+ //# sourceMappingURL=blocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzD,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,iBAAiB,CAAC;CAC/C,CAAC;AAEF,KAAK,QAAQ,CAAC,QAAQ,GAAG,SAAS,IAAI;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,iBAAiB,CAAC;CACnE,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,KAAK,OAAO,GACpE,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GACrB,SAAS,GACT,CAAC,GACH,KAAK,CAAC;AAEV,MAAM,MAAM,YAAY,CAAC,QAAQ,GAAG,SAAS,IAAI,CAC/C,OAAO,CAAC,EAAE,QAAQ,KACf,KAAK,CAAC;AAQX,wBAAgB,WAAW,CAAC,QAAQ,GAAG,SAAS,EAC9C,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,GACtB,YAAY,CAAC,QAAQ,CAAC,CAYxB;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAStD"}
package/dist/blocks.js ADDED
@@ -0,0 +1,25 @@
1
+ function validateBlockName(name) {
2
+ if (!name || name.length === 0) {
3
+ throw new Error("createBlock(def): name is required");
4
+ }
5
+ }
6
+ export function createBlock(def) {
7
+ validateBlockName(def.name);
8
+ if (typeof def.render !== "function") {
9
+ throw new Error("createBlock(def): render is required");
10
+ }
11
+ return ((options) => ({
12
+ __kind: "block",
13
+ name: def.name,
14
+ render: (ctx) => def.render(options, ctx),
15
+ }));
16
+ }
17
+ export function isBlock(value) {
18
+ return (typeof value === "object" &&
19
+ value !== null &&
20
+ value.__kind === "block" &&
21
+ typeof value.name === "string" &&
22
+ value.name.length > 0 &&
23
+ typeof value.render === "function");
24
+ }
25
+ //# sourceMappingURL=blocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AA0BA,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,GAAuB;IAEvB,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE5B,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,CAAC,CAAC,OAAkB,EAAE,EAAE,CAAC,CAAC;QAC/B,MAAM,EAAE,OAAgB;QACxB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,CAAC,GAAkB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,OAAmB,EAAE,GAAG,CAAC;KACrE,CAAC,CAA2B,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAe,CAAC,MAAM,KAAK,OAAO;QACnC,OAAQ,KAAe,CAAC,IAAI,KAAK,QAAQ;QACxC,KAAe,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAChC,OAAQ,KAAe,CAAC,MAAM,KAAK,UAAU,CAC9C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { html } from "@hyperspan/html";
2
+ export { createBlock, isBlock, type Block, type BlockFactory, type BlockOptionsOf, type BlockRenderResult, } from "./blocks.js";
3
+ export { createPage, isPage, PageValidationError, toPageRenderView, validateSlotContentItem, type Page, type PageMeta, type PageRenderView, type RenderContext, type SitePage, } from "./pages.js";
4
+ export { renderStandardHead } from "./render-head.js";
5
+ export { renderPage, RenderError } from "./render-page.js";
6
+ export { matchPagePath, normalizePathname } from "./routing.js";
7
+ export { createSite, createSiteConfig, isSite, isSiteConfig, SiteError, type Site, type SiteConfig, } from "./site.js";
8
+ export { createContext, Stackbox } from "./stackbox/context.js";
9
+ export { anySlotContentSchema, blockSlotContentSchema, slotHasContent, SlotContentValidationError, stringSlotContentSchema, type DefaultSlotContent, type PageSlotsInput, type SlotContentFromDefinition, } from "./slot-content.js";
10
+ export { buildPageSlots, buildStubSlots, renderSlotContent, slotSentinel, SLOT_SENTINEL_PREFIX, type Slot, type SlotRenderValue, type TemplateSlotsFrom, } from "./slot-handle.js";
11
+ export { createTemplate, isTemplate, TemplateBuildError, type RequiredSlotNamesFrom, type SlotDefinition, type SlotMeta, type SlotNamesFrom, type SlotOptions, type TemplateDescriptor, type TemplateRenderContext, type TemplateRenderFn, } from "./templates.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EACL,WAAW,EACX,OAAO,EACP,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,UAAU,EACV,MAAM,EACN,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,IAAI,EACT,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,YAAY,EACZ,SAAS,EACT,KAAK,IAAI,EACT,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,0BAA0B,EAC1B,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { html } from "@hyperspan/html";
2
+ export { createBlock, isBlock, } from "./blocks.js";
3
+ export { createPage, isPage, PageValidationError, toPageRenderView, validateSlotContentItem, } from "./pages.js";
4
+ export { renderStandardHead } from "./render-head.js";
5
+ export { renderPage, RenderError } from "./render-page.js";
6
+ export { matchPagePath, normalizePathname } from "./routing.js";
7
+ export { createSite, createSiteConfig, isSite, isSiteConfig, SiteError, } from "./site.js";
8
+ export { createContext } from "./stackbox/context.js";
9
+ export { anySlotContentSchema, blockSlotContentSchema, slotHasContent, SlotContentValidationError, stringSlotContentSchema, } from "./slot-content.js";
10
+ export { buildPageSlots, buildStubSlots, renderSlotContent, slotSentinel, SLOT_SENTINEL_PREFIX, } from "./slot-handle.js";
11
+ export { createTemplate, isTemplate, TemplateBuildError, } from "./templates.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EACL,WAAW,EACX,OAAO,GAKR,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,UAAU,EACV,MAAM,EACN,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,GAMxB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,YAAY,EACZ,SAAS,GAGV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAY,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,0BAA0B,EAC1B,uBAAuB,GAIxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,GAIrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,UAAU,EACV,kBAAkB,GASnB,MAAM,gBAAgB,CAAC"}
package/dist/link.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare const PLUGIN_PUBLIC_PREFIX = "/_sb/plugins";
2
+ export declare function pluginAssetPath(pluginName: string, relativePath: string): string;
3
+ export type PluginAssetRequest = {
4
+ plugin: string;
5
+ relativePath: string;
6
+ };
7
+ export declare function parsePluginAssetRequest(pathname: string): PluginAssetRequest | null;
8
+ export declare function getPluginPublicRootDir(): string;
9
+ export declare function servePluginAsset(pathname: string, method: string): globalThis.Response | null;
10
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AA8BnD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAShF;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GACf,kBAAkB,GAAG,IAAI,CAkC3B;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAa/C;AAwBD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,UAAU,CAAC,QAAQ,GAAG,IAAI,CA4B5B"}
package/dist/link.js ADDED
@@ -0,0 +1,128 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, normalize, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { normalizePathname } from "./routing.js";
5
+ export const PLUGIN_PUBLIC_PREFIX = "/_sb/plugins";
6
+ const CONTENT_TYPES = {
7
+ ".css": "text/css; charset=utf-8",
8
+ ".gif": "image/gif",
9
+ ".html": "text/html; charset=utf-8",
10
+ ".jpeg": "image/jpeg",
11
+ ".jpg": "image/jpeg",
12
+ ".js": "text/javascript; charset=utf-8",
13
+ ".json": "application/json; charset=utf-8",
14
+ ".png": "image/png",
15
+ ".svg": "image/svg+xml",
16
+ ".txt": "text/plain; charset=utf-8",
17
+ ".webp": "image/webp",
18
+ ".woff": "font/woff",
19
+ ".woff2": "font/woff2",
20
+ };
21
+ function assertSafeSegment(segment, label) {
22
+ if (!segment ||
23
+ segment === "." ||
24
+ segment === ".." ||
25
+ segment.includes("/") ||
26
+ segment.includes("\\")) {
27
+ throw new Error(`pluginAssetPath: invalid ${label}`);
28
+ }
29
+ }
30
+ export function pluginAssetPath(pluginName, relativePath) {
31
+ assertSafeSegment(pluginName, "plugin name");
32
+ const segments = relativePath
33
+ .split(/[/\\]+/)
34
+ .filter((segment) => segment.length > 0);
35
+ for (const segment of segments) {
36
+ assertSafeSegment(segment, "relative path segment");
37
+ }
38
+ return `${PLUGIN_PUBLIC_PREFIX}/${pluginName}/${segments.join("/")}`;
39
+ }
40
+ export function parsePluginAssetRequest(pathname) {
41
+ const normalized = normalizePathname(pathname);
42
+ const prefix = `${PLUGIN_PUBLIC_PREFIX}/`;
43
+ if (!normalized.startsWith(prefix)) {
44
+ return null;
45
+ }
46
+ const rest = normalized.slice(prefix.length);
47
+ const slash = rest.indexOf("/");
48
+ if (slash <= 0 || slash === rest.length - 1) {
49
+ return null;
50
+ }
51
+ const plugin = rest.slice(0, slash);
52
+ const relativePath = rest.slice(slash + 1);
53
+ if (!plugin ||
54
+ !relativePath ||
55
+ relativePath.includes("..") ||
56
+ relativePath.includes("\\")) {
57
+ return null;
58
+ }
59
+ try {
60
+ assertSafeSegment(plugin, "plugin name");
61
+ for (const segment of relativePath.split("/")) {
62
+ assertSafeSegment(segment, "relative path segment");
63
+ }
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ return { plugin, relativePath };
69
+ }
70
+ export function getPluginPublicRootDir() {
71
+ const here = dirname(fileURLToPath(import.meta.url));
72
+ const fromDist = join(here, "public", "_sb", "plugins");
73
+ if (existsSync(fromDist)) {
74
+ return fromDist;
75
+ }
76
+ const fromSrcDev = join(here, "..", "dist", "public", "_sb", "plugins");
77
+ if (existsSync(fromSrcDev)) {
78
+ return fromSrcDev;
79
+ }
80
+ return fromDist;
81
+ }
82
+ function contentTypeForPath(relativePath) {
83
+ const dot = relativePath.lastIndexOf(".");
84
+ if (dot === -1) {
85
+ return "application/octet-stream";
86
+ }
87
+ return CONTENT_TYPES[relativePath.slice(dot).toLowerCase()] ??
88
+ "application/octet-stream";
89
+ }
90
+ function resolvePluginAssetFile(request) {
91
+ const root = getPluginPublicRootDir();
92
+ const absolute = normalize(join(root, request.plugin, request.relativePath));
93
+ const rootWithSep = root.endsWith(sep) ? root : `${root}${sep}`;
94
+ if (!absolute.startsWith(rootWithSep)) {
95
+ return null;
96
+ }
97
+ if (!existsSync(absolute)) {
98
+ return null;
99
+ }
100
+ return absolute;
101
+ }
102
+ export function servePluginAsset(pathname, method) {
103
+ const request = parsePluginAssetRequest(pathname);
104
+ if (!request) {
105
+ return null;
106
+ }
107
+ const filePath = resolvePluginAssetFile(request);
108
+ if (!filePath) {
109
+ return new globalThis.Response("Not Found", { status: 404 });
110
+ }
111
+ const contentType = contentTypeForPath(request.relativePath);
112
+ if (method === "HEAD") {
113
+ const body = readFileSync(filePath);
114
+ return new globalThis.Response(null, {
115
+ status: 200,
116
+ headers: {
117
+ "content-type": contentType,
118
+ "content-length": String(body.byteLength),
119
+ },
120
+ });
121
+ }
122
+ const body = readFileSync(filePath);
123
+ return new globalThis.Response(body, {
124
+ status: 200,
125
+ headers: { "content-type": contentType },
126
+ });
127
+ }
128
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,cAAc,CAAC;AAEnD,MAAM,aAAa,GAA2B;IAC5C,MAAM,EAAE,yBAAyB;IACjC,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,0BAA0B;IACnC,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,YAAY;IACpB,KAAK,EAAE,gCAAgC;IACvC,OAAO,EAAE,iCAAiC;IAC1C,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,2BAA2B;IACnC,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;CACvB,CAAC;AAEF,SAAS,iBAAiB,CAAC,OAAe,EAAE,KAAa;IACvD,IACE,CAAC,OAAO;QACR,OAAO,KAAK,GAAG;QACf,OAAO,KAAK,IAAI;QAChB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,4BAA4B,KAAK,EAAE,CAAC,CAAC;IACvD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,YAAoB;IACtE,iBAAiB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,YAAY;SAC1B,KAAK,CAAC,QAAQ,CAAC;SACf,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,iBAAiB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,GAAG,oBAAoB,IAAI,UAAU,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACvE,CAAC;AAOD,MAAM,UAAU,uBAAuB,CACrC,QAAgB;IAEhB,MAAM,UAAU,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,GAAG,oBAAoB,GAAG,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACpC,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC3C,IACE,CAAC,MAAM;QACP,CAAC,YAAY;QACb,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,EAC3B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,iBAAiB,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACzC,KAAK,MAAM,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9C,iBAAiB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IACxD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IACxE,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,YAAoB;IAC9C,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QACf,OAAO,0BAA0B,CAAC;IACpC,CAAC;IACD,OAAO,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QACzD,0BAA0B,CAAC;AAC/B,CAAC;AAED,SAAS,sBAAsB,CAAC,OAA2B;IACzD,MAAM,IAAI,GAAG,sBAAsB,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC;IAChE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,QAAgB,EAChB,MAAc;IAEd,MAAM,OAAO,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE;YACnC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,cAAc,EAAE,WAAW;gBAC3B,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;aAC1C;SACF,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE;QACnC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE;KACzC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { PageMeta } from "../../pages.js";
2
+ import type { DefaultSlotContent } from "../../slot-content.js";
3
+ import { type BlogOptions } from "./posts.js";
4
+ export type { BlogPost, BlogOptions } from "./posts.js";
5
+ export type BlogPostContent = {
6
+ path: string;
7
+ title: string;
8
+ meta?: PageMeta;
9
+ content: DefaultSlotContent[];
10
+ };
11
+ export type BlogListingContent = {
12
+ path: string;
13
+ content: DefaultSlotContent[];
14
+ };
15
+ export type Blog = {
16
+ readonly posts: readonly BlogPostContent[];
17
+ readonly listings: readonly BlogListingContent[];
18
+ };
19
+ export declare function createBlog(options: BlogOptions): Blog;
20
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../src/modules/blog/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAa,KAAK,WAAW,EAAiB,MAAM,YAAY,CAAC;AAExE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAClD,CAAC;AAwDF,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAQrD"}
@@ -0,0 +1,52 @@
1
+ import { loadPosts } from "./posts.js";
2
+ function postToContent(post) {
3
+ return {
4
+ path: post.path,
5
+ title: post.title,
6
+ meta: post.description ? { description: post.description } : undefined,
7
+ content: [
8
+ `<article data-content-type="article" data-slug="${post.slug}">
9
+ <h1>${post.title}</h1>
10
+ ${post.bodyHtml}
11
+ </article>`,
12
+ ],
13
+ };
14
+ }
15
+ function listingPath(pathPrefix, page) {
16
+ return page === 1 ? pathPrefix : `${pathPrefix}/page/${page}`;
17
+ }
18
+ function buildPostListHtml(posts) {
19
+ return `<ul>${posts
20
+ .map((post) => `<li><a href="${post.path}">${post.title}</a></li>`)
21
+ .join("")}</ul>`;
22
+ }
23
+ function buildListingPages(posts, pathPrefix, postsPerPage) {
24
+ if (posts.length === 0) {
25
+ return [
26
+ {
27
+ path: pathPrefix,
28
+ content: ["<p>No blog posts found.</p>"],
29
+ },
30
+ ];
31
+ }
32
+ const perPage = postsPerPage && postsPerPage > 0 ? postsPerPage : posts.length;
33
+ const pages = [];
34
+ for (let i = 0; i < posts.length; i += perPage) {
35
+ const chunk = posts.slice(i, i + perPage);
36
+ const page = i / perPage + 1;
37
+ pages.push({
38
+ path: listingPath(pathPrefix, page),
39
+ content: [buildPostListHtml(chunk)],
40
+ });
41
+ }
42
+ return pages;
43
+ }
44
+ export function createBlog(options) {
45
+ const loaded = loadPosts(options);
46
+ const pathPrefix = options.pathPrefix.replace(/\/+$/, "") || "/";
47
+ return {
48
+ posts: loaded.map(postToContent),
49
+ listings: buildListingPages(loaded, pathPrefix, options.postsPerPage),
50
+ };
51
+ }
52
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/modules/blog/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAqBxE,SAAS,aAAa,CAAC,IAAc;IACnC,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS;QACtE,OAAO,EAAE;YACP,mDAAmD,IAAI,CAAC,IAAI;QAC1D,IAAI,CAAC,KAAK;IACd,IAAI,CAAC,QAAQ;WACN;SACN;KACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,UAAkB,EAAE,IAAY;IACnD,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,SAAS,IAAI,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAiB;IAC1C,OAAO,OAAO,KAAK;SAChB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,WAAW,CAAC;SAClE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;AACrB,CAAC;AAED,SAAS,iBAAiB,CACxB,KAAiB,EACjB,UAAkB,EAClB,YAAqB;IAErB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO;YACL;gBACE,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,CAAC,6BAA6B,CAAC;aACzC;SACF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GACX,YAAY,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;IACjE,MAAM,KAAK,GAAyB,EAAE,CAAC;IAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC;YACnC,OAAO,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;SACpC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAoB;IAC7C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC;IAEjE,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;QAChC,QAAQ,EAAE,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC;KACtE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ export type BlogPost = {
2
+ slug: string;
3
+ title: string;
4
+ description?: string;
5
+ path: string;
6
+ bodyHtml: string;
7
+ };
8
+ export type BlogOptions = {
9
+ contentPath: string;
10
+ pathPrefix: string;
11
+ /** Max posts per listing page. Omit to put all posts on one page. */
12
+ postsPerPage?: number;
13
+ };
14
+ export declare function loadPosts(options: BlogOptions): BlogPost[];
15
+ //# sourceMappingURL=posts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posts.d.ts","sourceRoot":"","sources":["../../../src/modules/blog/posts.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA8BF,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,EAAE,CAqB1D"}
@@ -0,0 +1,46 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { basename, extname, join, resolve } from "node:path";
3
+ import { marked } from "marked";
4
+ function parseFrontmatter(raw) {
5
+ if (!raw.startsWith("---")) {
6
+ return { meta: {}, body: raw };
7
+ }
8
+ const end = raw.indexOf("\n---", 3);
9
+ if (end === -1) {
10
+ return { meta: {}, body: raw };
11
+ }
12
+ const frontmatter = raw.slice(3, end).trim();
13
+ const body = raw.slice(end + 4).replace(/^\n/, "");
14
+ const meta = {};
15
+ for (const line of frontmatter.split("\n")) {
16
+ const colon = line.indexOf(":");
17
+ if (colon === -1)
18
+ continue;
19
+ const key = line.slice(0, colon).trim();
20
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
21
+ if (key)
22
+ meta[key] = value;
23
+ }
24
+ return { meta, body };
25
+ }
26
+ export function loadPosts(options) {
27
+ const absoluteDir = resolve(options.contentPath);
28
+ const files = readdirSync(absoluteDir)
29
+ .filter((name) => extname(name) === ".md")
30
+ .sort();
31
+ return files.map((filename) => {
32
+ const raw = readFileSync(join(absoluteDir, filename), "utf8");
33
+ const { meta, body } = parseFrontmatter(raw);
34
+ const slug = meta.slug ?? basename(filename, ".md");
35
+ const title = meta.title ?? slug;
36
+ const prefix = options.pathPrefix.replace(/\/+$/, "");
37
+ return {
38
+ slug,
39
+ title,
40
+ description: meta.description,
41
+ path: `${prefix}/${slug}`,
42
+ bodyHtml: marked.parse(body),
43
+ };
44
+ });
45
+ }
46
+ //# sourceMappingURL=posts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posts.js","sourceRoot":"","sources":["../../../src/modules/blog/posts.ts"],"names":[],"mappings":"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"}