@stackbox/cms 0.0.4 → 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 (113) hide show
  1. package/dist/blocks.d.ts +18 -0
  2. package/dist/blocks.d.ts.map +1 -0
  3. package/dist/blocks.js +25 -0
  4. package/dist/blocks.js.map +1 -0
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +12 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/link.d.ts +10 -0
  10. package/dist/link.d.ts.map +1 -0
  11. package/dist/link.js +128 -0
  12. package/dist/link.js.map +1 -0
  13. package/dist/modules/blog/module.d.ts +20 -0
  14. package/dist/modules/blog/module.d.ts.map +1 -0
  15. package/dist/modules/blog/module.js +52 -0
  16. package/dist/modules/blog/module.js.map +1 -0
  17. package/dist/modules/blog/posts.d.ts +15 -0
  18. package/dist/modules/blog/posts.d.ts.map +1 -0
  19. package/dist/modules/blog/posts.js +46 -0
  20. package/dist/modules/blog/posts.js.map +1 -0
  21. package/dist/modules.d.ts +18 -0
  22. package/dist/modules.d.ts.map +1 -0
  23. package/dist/modules.js +25 -0
  24. package/dist/modules.js.map +1 -0
  25. package/dist/pages.d.ts +55 -0
  26. package/dist/pages.d.ts.map +1 -0
  27. package/dist/pages.js +85 -0
  28. package/dist/pages.js.map +1 -0
  29. package/dist/plugins/blog/index.d.ts +20 -0
  30. package/dist/plugins/blog/index.d.ts.map +1 -0
  31. package/dist/plugins/blog/index.js +52 -0
  32. package/dist/plugins/blog/index.js.map +1 -0
  33. package/dist/plugins/blog/module.d.ts +20 -0
  34. package/dist/plugins/blog/module.d.ts.map +1 -0
  35. package/dist/plugins/blog/module.js +52 -0
  36. package/dist/plugins/blog/module.js.map +1 -0
  37. package/dist/plugins/blog/plugin.d.ts +20 -0
  38. package/dist/plugins/blog/plugin.d.ts.map +1 -0
  39. package/dist/plugins/blog/plugin.js +52 -0
  40. package/dist/plugins/blog/plugin.js.map +1 -0
  41. package/dist/plugins/blog/posts.d.ts +15 -0
  42. package/dist/plugins/blog/posts.d.ts.map +1 -0
  43. package/dist/plugins/blog/posts.js +46 -0
  44. package/dist/plugins/blog/posts.js.map +1 -0
  45. package/dist/plugins/random-quote/index.d.ts +12 -0
  46. package/dist/plugins/random-quote/index.d.ts.map +1 -0
  47. package/dist/plugins/random-quote/index.js +21 -0
  48. package/dist/plugins/random-quote/index.js.map +1 -0
  49. package/dist/plugins/random-quote/module.d.ts +9 -0
  50. package/dist/plugins/random-quote/module.d.ts.map +1 -0
  51. package/dist/plugins/random-quote/module.js +31 -0
  52. package/dist/plugins/random-quote/module.js.map +1 -0
  53. package/dist/plugins/random-quote/plugin.d.ts +8 -0
  54. package/dist/plugins/random-quote/plugin.d.ts.map +1 -0
  55. package/dist/plugins/random-quote/plugin.js +31 -0
  56. package/dist/plugins/random-quote/plugin.js.map +1 -0
  57. package/dist/plugins/random-quote/public_assets/quotes.json +230 -0
  58. package/dist/plugins/random-quote/quotes.json +230 -0
  59. package/dist/public/_sb/plugins/random-quote/quotes.json +230 -0
  60. package/dist/render-head.d.ts +7 -0
  61. package/dist/render-head.d.ts.map +1 -0
  62. package/dist/render-head.js +27 -0
  63. package/dist/render-head.js.map +1 -0
  64. package/dist/render-page.d.ts +6 -0
  65. package/dist/render-page.d.ts.map +1 -0
  66. package/dist/render-page.js +30 -0
  67. package/dist/render-page.js.map +1 -0
  68. package/dist/routing.d.ts +3 -0
  69. package/dist/routing.d.ts.map +1 -0
  70. package/dist/routing.js +11 -0
  71. package/dist/routing.js.map +1 -0
  72. package/dist/site.d.ts +21 -0
  73. package/dist/site.d.ts.map +1 -0
  74. package/dist/site.js +102 -0
  75. package/dist/site.js.map +1 -0
  76. package/dist/slot-content.d.ts +30 -0
  77. package/dist/slot-content.d.ts.map +1 -0
  78. package/dist/slot-content.js +34 -0
  79. package/dist/slot-content.js.map +1 -0
  80. package/dist/slot-handle.d.ts +32 -0
  81. package/dist/slot-handle.d.ts.map +1 -0
  82. package/dist/slot-handle.js +68 -0
  83. package/dist/slot-handle.js.map +1 -0
  84. package/dist/stackbox/context.d.ts +52 -0
  85. package/dist/stackbox/context.d.ts.map +1 -0
  86. package/dist/stackbox/context.js +165 -0
  87. package/dist/stackbox/context.js.map +1 -0
  88. package/dist/templates.d.ts +63 -0
  89. package/dist/templates.d.ts.map +1 -0
  90. package/dist/templates.js +106 -0
  91. package/dist/templates.js.map +1 -0
  92. package/package.json +7 -2
  93. package/.claude/settings.local.json +0 -8
  94. package/scripts/copy-plugin-assets.mjs +0 -30
  95. package/src/blocks.ts +0 -58
  96. package/src/index.ts +0 -67
  97. package/src/link.ts +0 -158
  98. package/src/pages.ts +0 -198
  99. package/src/render-head.ts +0 -32
  100. package/src/render-page.ts +0 -51
  101. package/src/routing.ts +0 -14
  102. package/src/site.ts +0 -160
  103. package/src/slot-content.ts +0 -77
  104. package/src/slot-handle.ts +0 -131
  105. package/src/stackbox/context.ts +0 -257
  106. package/src/templates.ts +0 -230
  107. package/test/async-render.test.ts +0 -103
  108. package/test/blog.test.ts +0 -93
  109. package/test/fetch-routing.test.ts +0 -76
  110. package/test/plugin-assets.test.ts +0 -80
  111. package/test/random-quote.test.ts +0 -55
  112. package/test/stackbox-context.test.ts +0 -44
  113. package/tsconfig.json +0 -20
package/src/link.ts DELETED
@@ -1,158 +0,0 @@
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
-
6
- export const PLUGIN_PUBLIC_PREFIX = "/_sb/plugins";
7
-
8
- const CONTENT_TYPES: Record<string, string> = {
9
- ".css": "text/css; charset=utf-8",
10
- ".gif": "image/gif",
11
- ".html": "text/html; charset=utf-8",
12
- ".jpeg": "image/jpeg",
13
- ".jpg": "image/jpeg",
14
- ".js": "text/javascript; charset=utf-8",
15
- ".json": "application/json; charset=utf-8",
16
- ".png": "image/png",
17
- ".svg": "image/svg+xml",
18
- ".txt": "text/plain; charset=utf-8",
19
- ".webp": "image/webp",
20
- ".woff": "font/woff",
21
- ".woff2": "font/woff2",
22
- };
23
-
24
- function assertSafeSegment(segment: string, label: string): void {
25
- if (
26
- !segment ||
27
- segment === "." ||
28
- segment === ".." ||
29
- segment.includes("/") ||
30
- segment.includes("\\")
31
- ) {
32
- throw new Error(`pluginAssetPath: invalid ${label}`);
33
- }
34
- }
35
-
36
- export function pluginAssetPath(pluginName: string, relativePath: string): string {
37
- assertSafeSegment(pluginName, "plugin name");
38
- const segments = relativePath
39
- .split(/[/\\]+/)
40
- .filter((segment) => segment.length > 0);
41
- for (const segment of segments) {
42
- assertSafeSegment(segment, "relative path segment");
43
- }
44
- return `${PLUGIN_PUBLIC_PREFIX}/${pluginName}/${segments.join("/")}`;
45
- }
46
-
47
- export type PluginAssetRequest = {
48
- plugin: string;
49
- relativePath: string;
50
- };
51
-
52
- export function parsePluginAssetRequest(
53
- pathname: string,
54
- ): PluginAssetRequest | null {
55
- const normalized = normalizePathname(pathname);
56
- const prefix = `${PLUGIN_PUBLIC_PREFIX}/`;
57
- if (!normalized.startsWith(prefix)) {
58
- return null;
59
- }
60
-
61
- const rest = normalized.slice(prefix.length);
62
- const slash = rest.indexOf("/");
63
- if (slash <= 0 || slash === rest.length - 1) {
64
- return null;
65
- }
66
-
67
- const plugin = rest.slice(0, slash);
68
- const relativePath = rest.slice(slash + 1);
69
- if (
70
- !plugin ||
71
- !relativePath ||
72
- relativePath.includes("..") ||
73
- relativePath.includes("\\")
74
- ) {
75
- return null;
76
- }
77
-
78
- try {
79
- assertSafeSegment(plugin, "plugin name");
80
- for (const segment of relativePath.split("/")) {
81
- assertSafeSegment(segment, "relative path segment");
82
- }
83
- } catch {
84
- return null;
85
- }
86
-
87
- return { plugin, relativePath };
88
- }
89
-
90
- export function getPluginPublicRootDir(): string {
91
- const here = dirname(fileURLToPath(import.meta.url));
92
- const fromDist = join(here, "public", "_sb", "plugins");
93
- if (existsSync(fromDist)) {
94
- return fromDist;
95
- }
96
-
97
- const fromSrcDev = join(here, "..", "dist", "public", "_sb", "plugins");
98
- if (existsSync(fromSrcDev)) {
99
- return fromSrcDev;
100
- }
101
-
102
- return fromDist;
103
- }
104
-
105
- function contentTypeForPath(relativePath: string): string {
106
- const dot = relativePath.lastIndexOf(".");
107
- if (dot === -1) {
108
- return "application/octet-stream";
109
- }
110
- return CONTENT_TYPES[relativePath.slice(dot).toLowerCase()] ??
111
- "application/octet-stream";
112
- }
113
-
114
- function resolvePluginAssetFile(request: PluginAssetRequest): string | null {
115
- const root = getPluginPublicRootDir();
116
- const absolute = normalize(join(root, request.plugin, request.relativePath));
117
- const rootWithSep = root.endsWith(sep) ? root : `${root}${sep}`;
118
- if (!absolute.startsWith(rootWithSep)) {
119
- return null;
120
- }
121
- if (!existsSync(absolute)) {
122
- return null;
123
- }
124
- return absolute;
125
- }
126
-
127
- export function servePluginAsset(
128
- pathname: string,
129
- method: string,
130
- ): globalThis.Response | null {
131
- const request = parsePluginAssetRequest(pathname);
132
- if (!request) {
133
- return null;
134
- }
135
-
136
- const filePath = resolvePluginAssetFile(request);
137
- if (!filePath) {
138
- return new globalThis.Response("Not Found", { status: 404 });
139
- }
140
-
141
- const contentType = contentTypeForPath(request.relativePath);
142
- if (method === "HEAD") {
143
- const body = readFileSync(filePath);
144
- return new globalThis.Response(null, {
145
- status: 200,
146
- headers: {
147
- "content-type": contentType,
148
- "content-length": String(body.byteLength),
149
- },
150
- });
151
- }
152
-
153
- const body = readFileSync(filePath);
154
- return new globalThis.Response(body, {
155
- status: 200,
156
- headers: { "content-type": contentType },
157
- });
158
- }
package/src/pages.ts DELETED
@@ -1,198 +0,0 @@
1
- import type { SiteConfig } from "./site.js";
2
- import type { Stackbox } from "./stackbox/context.js";
3
- import {
4
- isTemplate,
5
- type SlotDefinition,
6
- type SlotNamesFrom,
7
- type RequiredSlotNamesFrom,
8
- type TemplateDescriptor,
9
- } from "./templates.js";
10
- import {
11
- type DefaultSlotContent,
12
- type PageSlotsInput,
13
- slotHasContent,
14
- SlotContentValidationError,
15
- validateSlotContentItem,
16
- } from "./slot-content.js";
17
-
18
- export type PageMeta = {
19
- description?: string;
20
- robots?: string;
21
- canonical?: string;
22
- ogTitle?: string;
23
- ogDescription?: string;
24
- ogImage?: string;
25
- twitterCard?: string;
26
- };
27
-
28
- export type RenderContext = {
29
- siteConfig: SiteConfig;
30
- ctx?: Stackbox.Context;
31
- };
32
-
33
- export type Page<
34
- Slots extends string = string,
35
- RequiredSlots extends Slots = never,
36
- Definitions extends readonly SlotDefinition[] = readonly SlotDefinition[],
37
- > = {
38
- readonly __kind: "page";
39
- path: string;
40
- template: TemplateDescriptor<Slots, RequiredSlots, Definitions>;
41
- title: string;
42
- meta?: PageMeta;
43
- slots: PageSlotsInput<Definitions, RequiredSlots>;
44
- };
45
-
46
- /** Widened page type for site builds mixing templates and required slots. */
47
- export type SitePage = {
48
- readonly __kind: "page";
49
- path: string;
50
- template: TemplateDescriptor<string, string>;
51
- title: string;
52
- meta?: PageMeta;
53
- slots: Partial<Record<string, DefaultSlotContent[]>>;
54
- };
55
-
56
- /** Page fields passed to template render (slot content lives on `slots`). */
57
- export type PageRenderView = Omit<Page, "template" | "slots">;
58
-
59
- export function toPageRenderView({
60
- template: _template,
61
- slots: _slots,
62
- ...view
63
- }: SitePage): PageRenderView {
64
- return view;
65
- }
66
-
67
- export function isPage(value: unknown): value is Page {
68
- return (
69
- typeof value === "object" &&
70
- value !== null &&
71
- (value as Page).__kind === "page" &&
72
- typeof (value as Page).path === "string" &&
73
- (value as Page).path.length > 0 &&
74
- isTemplate((value as Page).template) &&
75
- typeof (value as Page).title === "string" &&
76
- (value as Page).title.length > 0 &&
77
- typeof (value as Page).slots === "object" &&
78
- (value as Page).slots !== null
79
- );
80
- }
81
-
82
- export class PageValidationError extends Error {
83
- constructor(message: string) {
84
- super(message);
85
- this.name = "PageValidationError";
86
- }
87
- }
88
-
89
- function validatePagePath(path: string): void {
90
- if (!path || !path.startsWith("/")) {
91
- throw new PageValidationError('path must start with "/"');
92
- }
93
- if (path.includes("..")) {
94
- throw new PageValidationError("path must not contain '..'");
95
- }
96
- const lastSegment = path.split("/").pop() ?? "";
97
- if (lastSegment.includes(".")) {
98
- throw new PageValidationError("path must not include a file extension");
99
- }
100
- }
101
-
102
- function validatePageSlotContent(
103
- template: TemplateDescriptor<string, string>,
104
- slots: Partial<Record<string, DefaultSlotContent[]>>,
105
- ): void {
106
- const templateSlotNames = new Set(Object.keys(template.slots));
107
-
108
- for (const key of Object.keys(slots)) {
109
- if (!templateSlotNames.has(key)) {
110
- throw new PageValidationError(`unknown slot "${key}" for this template`);
111
- }
112
-
113
- const items = slots[key];
114
- if (!Array.isArray(items)) {
115
- throw new PageValidationError(
116
- `slot "${key}" must be an array of content items`,
117
- );
118
- }
119
-
120
- const schema = template.slots[key]?.schema;
121
- for (const item of items) {
122
- try {
123
- validateSlotContentItem(key, item, schema);
124
- } catch (err) {
125
- if (err instanceof SlotContentValidationError) {
126
- throw new PageValidationError(err.message);
127
- }
128
- throw err;
129
- }
130
- }
131
- }
132
- }
133
-
134
- export function createPage(
135
- template: TemplateDescriptor<string, string>,
136
- def: {
137
- path: string;
138
- title: string;
139
- meta?: PageMeta;
140
- slots: Partial<Record<string, DefaultSlotContent[]>>;
141
- },
142
- ): SitePage;
143
- export function createPage<const S extends readonly SlotDefinition[]>(
144
- template: TemplateDescriptor<
145
- SlotNamesFrom<S>,
146
- RequiredSlotNamesFrom<S>,
147
- S
148
- >,
149
- def: {
150
- path: string;
151
- title: string;
152
- meta?: PageMeta;
153
- slots: PageSlotsInput<S, RequiredSlotNamesFrom<S>>;
154
- },
155
- ): Page<SlotNamesFrom<S>, RequiredSlotNamesFrom<S>, S>;
156
- export function createPage(
157
- template: TemplateDescriptor<string, string>,
158
- def: {
159
- path: string;
160
- title: string;
161
- meta?: PageMeta;
162
- slots: Partial<Record<string, DefaultSlotContent[]>>;
163
- },
164
- ): SitePage {
165
- if (!isTemplate(template)) {
166
- throw new PageValidationError(
167
- "template must be created with createTemplate()",
168
- );
169
- }
170
-
171
- validatePagePath(def.path);
172
-
173
- if (!def.title || def.title.trim().length === 0) {
174
- throw new PageValidationError("title is required");
175
- }
176
-
177
- validatePageSlotContent(template, def.slots);
178
-
179
- for (const required of template.requiredSlots) {
180
- const items = def.slots[required as keyof typeof def.slots];
181
- if (!items || items.length === 0 || !slotHasContent(items)) {
182
- throw new PageValidationError(
183
- `required slot "${required}" must have at least one block or non-empty HTML string`,
184
- );
185
- }
186
- }
187
-
188
- return {
189
- __kind: "page" as const,
190
- path: def.path,
191
- template,
192
- title: def.title,
193
- meta: def.meta,
194
- slots: def.slots,
195
- };
196
- }
197
-
198
- export { validateSlotContentItem } from "./slot-content.js";
@@ -1,32 +0,0 @@
1
- import { html, type HSHtml } from "@hyperspan/html";
2
- import type { PageMeta } from "./pages.js";
3
-
4
- export function renderStandardHead(input: {
5
- title: string;
6
- meta?: PageMeta;
7
- }): HSHtml {
8
- const { title, meta = {} } = input;
9
- const ogTitle = meta.ogTitle ?? title;
10
- const ogDescription = meta.ogDescription ?? meta.description;
11
-
12
- return html`<meta charset="utf-8">
13
- <meta name="viewport" content="width=device-width, initial-scale=1">
14
- <title>${title}</title>
15
- ${meta.description
16
- ? html`<meta name="description" content="${meta.description}">`
17
- : ""}
18
- ${meta.robots ? html`<meta name="robots" content="${meta.robots}">` : ""}
19
- ${meta.canonical
20
- ? html`<link rel="canonical" href="${meta.canonical}">`
21
- : ""}
22
- <meta property="og:title" content="${ogTitle}">
23
- ${ogDescription
24
- ? html`<meta property="og:description" content="${ogDescription}">`
25
- : ""}
26
- ${meta.ogImage
27
- ? html`<meta property="og:image" content="${meta.ogImage}">`
28
- : ""}
29
- ${meta.twitterCard
30
- ? html`<meta name="twitter:card" content="${meta.twitterCard}">`
31
- : ""}`;
32
- }
@@ -1,51 +0,0 @@
1
- import { renderAsync } from "@hyperspan/html";
2
- import {
3
- type SitePage,
4
- type RenderContext,
5
- toPageRenderView,
6
- PageValidationError,
7
- } from "./pages.js";
8
- import type { SiteConfig } from "./site.js";
9
- import type { Stackbox } from "./stackbox/context.js";
10
- import { slotHasContent } from "./slot-content.js";
11
- import { buildPageSlots, RenderError } from "./slot-handle.js";
12
- import { renderStandardHead } from "./render-head.js";
13
-
14
- export { RenderError } from "./slot-handle.js";
15
-
16
- function validateRequiredSlots(page: SitePage): void {
17
- for (const required of page.template.requiredSlots) {
18
- const items = page.slots[required as keyof typeof page.slots];
19
- if (!items || items.length === 0 || !slotHasContent(items)) {
20
- throw new PageValidationError(
21
- `required slot "${required}" must have at least one block or non-empty HTML string`,
22
- );
23
- }
24
- }
25
- }
26
-
27
- export async function renderPage(
28
- page: SitePage,
29
- siteConfig: SiteConfig,
30
- ctx?: Stackbox.Context,
31
- ): Promise<string> {
32
- validateRequiredSlots(page);
33
-
34
- const renderCtx: RenderContext = { siteConfig, ctx };
35
-
36
- const slots = buildPageSlots(page, renderCtx);
37
-
38
- const head = renderStandardHead({
39
- title: page.template.formatPageTitle(page.title),
40
- meta: page.meta,
41
- });
42
-
43
- return renderAsync(
44
- page.template.render({
45
- head,
46
- siteConfig,
47
- page: toPageRenderView(page),
48
- slots,
49
- }),
50
- );
51
- }
package/src/routing.ts DELETED
@@ -1,14 +0,0 @@
1
- export function normalizePathname(pathname: string): string {
2
- if (pathname === "" || pathname === "/") {
3
- return "/";
4
- }
5
- const trimmed = pathname.replace(/\/+$/, "");
6
- return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
7
- }
8
-
9
- export function matchPagePath(
10
- pathname: string,
11
- pagePath: string,
12
- ): boolean {
13
- return normalizePathname(pathname) === normalizePathname(pagePath);
14
- }
package/src/site.ts DELETED
@@ -1,160 +0,0 @@
1
- import type { SitePage } from "./pages.js";
2
- import { isPage } from "./pages.js";
3
- import { renderPage } from "./render-page.js";
4
- import { servePluginAsset } from "./link.js";
5
- import { normalizePathname } from "./routing.js";
6
- import { createContext } from "./stackbox/context.js";
7
-
8
- export type SiteConfig<
9
- T extends Record<string, unknown> = Record<string, unknown>,
10
- > = {
11
- readonly __kind: "siteConfig";
12
- readonly config: T;
13
- };
14
-
15
- export type Site<
16
- T extends Record<string, unknown> = Record<string, unknown>,
17
- > = {
18
- readonly __kind: "site";
19
- readonly siteConfig: SiteConfig<T>;
20
- readonly pages: readonly SitePage[];
21
- fetch(
22
- request: globalThis.Request,
23
- env?: Record<string, unknown>,
24
- ): Promise<globalThis.Response>;
25
- };
26
-
27
- export class SiteError extends Error {
28
- constructor(message: string) {
29
- super(message);
30
- this.name = "SiteError";
31
- }
32
- }
33
-
34
- function validateConfigObject(config: unknown, label: string): void {
35
- if (!config || typeof config !== "object" || Array.isArray(config)) {
36
- throw new SiteError(`${label}: config must be a non-null object`);
37
- }
38
- if (Object.keys(config).length === 0) {
39
- throw new SiteError(`${label}: config must not be empty`);
40
- }
41
- }
42
-
43
- export function createSiteConfig<T extends Record<string, unknown>>(
44
- config: T,
45
- ): SiteConfig<T> {
46
- validateConfigObject(config, "createSiteConfig(config)");
47
- return {
48
- __kind: "siteConfig" as const,
49
- config,
50
- };
51
- }
52
-
53
- export function isSiteConfig(value: unknown): value is SiteConfig {
54
- return (
55
- typeof value === "object" &&
56
- value !== null &&
57
- (value as SiteConfig).__kind === "siteConfig" &&
58
- typeof (value as SiteConfig).config === "object" &&
59
- (value as SiteConfig).config !== null &&
60
- !Array.isArray((value as SiteConfig).config)
61
- );
62
- }
63
-
64
- function assertUniquePaths(pages: SitePage[]): void {
65
- const seen = new Map<string, string>();
66
-
67
- for (const page of pages) {
68
- const existing = seen.get(page.path);
69
- if (existing) {
70
- throw new SiteError(
71
- `duplicate page path "${page.path}" (${existing} and ${page.title})`,
72
- );
73
- }
74
- seen.set(page.path, page.title);
75
- }
76
- }
77
-
78
- function buildPageMap(pages: readonly SitePage[]): Map<string, SitePage> {
79
- const map = new Map<string, SitePage>();
80
- for (const page of pages) {
81
- map.set(normalizePathname(page.path), page);
82
- }
83
- return map;
84
- }
85
-
86
- export function createSite<T extends Record<string, unknown>>(
87
- siteConfig: SiteConfig<T>,
88
- options: { pages: [SitePage, ...SitePage[]] },
89
- ): Site<T> {
90
- if (!isSiteConfig(siteConfig)) {
91
- throw new SiteError(
92
- "createSite(siteConfig, options): siteConfig must be from createSiteConfig()",
93
- );
94
- }
95
-
96
- const { pages } = options;
97
- if (!Array.isArray(pages) || pages.length === 0) {
98
- throw new SiteError(
99
- "createSite(siteConfig, options): pages must be a non-empty array",
100
- );
101
- }
102
-
103
- for (const page of pages) {
104
- if (!isPage(page)) {
105
- throw new SiteError(
106
- "createSite(siteConfig, options): every page must be from createPage()",
107
- );
108
- }
109
- }
110
-
111
- assertUniquePaths(pages);
112
-
113
- const pageMap = buildPageMap(pages);
114
-
115
- return {
116
- __kind: "site" as const,
117
- siteConfig,
118
- pages,
119
- async fetch(request, env = {}) {
120
- const ctx = createContext(request, env);
121
- const method = ctx.req.method;
122
-
123
- if (method !== "GET" && method !== "HEAD") {
124
- return ctx.res.text("Method Not Allowed", { status: 405 });
125
- }
126
-
127
- const pathname = normalizePathname(ctx.req.url.pathname);
128
-
129
- const pluginAsset = servePluginAsset(pathname, method);
130
- if (pluginAsset) {
131
- return pluginAsset;
132
- }
133
-
134
- const page = pageMap.get(pathname);
135
-
136
- if (!page) {
137
- return ctx.res.notFound();
138
- }
139
-
140
- const html = await renderPage(page, siteConfig, ctx);
141
-
142
- if (method === "HEAD") {
143
- return ctx.res.html("", { status: 200 });
144
- }
145
-
146
- return ctx.res.html(html);
147
- },
148
- };
149
- }
150
-
151
- export function isSite(value: unknown): value is Site {
152
- return (
153
- typeof value === "object" &&
154
- value !== null &&
155
- (value as Site).__kind === "site" &&
156
- isSiteConfig((value as Site).siteConfig) &&
157
- Array.isArray((value as Site).pages) &&
158
- typeof (value as Site).fetch === "function"
159
- );
160
- }
@@ -1,77 +0,0 @@
1
- import type { z } from "zod";
2
- import { z as zod } from "zod";
3
- import { isBlock, type Block } from "./blocks.js";
4
- import type { SlotDefinition, SlotNamesFrom } from "./templates.js";
5
-
6
- export type DefaultSlotContent = Block | string;
7
-
8
- /** Accepts any block or HTML string (default slot content). */
9
- export const anySlotContentSchema: z.ZodType<DefaultSlotContent> = zod.union([
10
- zod.string(),
11
- zod.custom<Block>((value) => isBlock(value)),
12
- ]);
13
-
14
- /** Accepts only HTML strings. */
15
- export const stringSlotContentSchema = zod.string();
16
-
17
- /** Accepts only CMS blocks. */
18
- export const blockSlotContentSchema: z.ZodType<Block> = zod.custom<Block>(
19
- (value) => isBlock(value),
20
- );
21
-
22
- export type SlotContentFromDefinition<D extends SlotDefinition> =
23
- D extends { options: { schema: infer Schema extends z.ZodTypeAny } }
24
- ? z.input<Schema>
25
- : DefaultSlotContent;
26
-
27
- export type PageSlotsInput<
28
- S extends readonly SlotDefinition[],
29
- Required extends string,
30
- > = {
31
- [K in Required]: SlotContentFromDefinition<
32
- Extract<S[number], { name: K }>
33
- >[];
34
- } & Partial<{
35
- [K in Exclude<SlotNamesFrom<S>, Required>]: SlotContentFromDefinition<
36
- Extract<S[number], { name: K }>
37
- >[];
38
- }>;
39
-
40
- export function validateSlotContentItem(
41
- slotName: string,
42
- item: unknown,
43
- schema: z.ZodTypeAny | undefined,
44
- ): void {
45
- if (schema) {
46
- const result = schema.safeParse(item);
47
- if (!result.success) {
48
- throw new SlotContentValidationError(
49
- slotName,
50
- result.error.message,
51
- );
52
- }
53
- return;
54
- }
55
-
56
- if (typeof item !== "string" && !isBlock(item)) {
57
- throw new SlotContentValidationError(
58
- slotName,
59
- "expected block or HTML string",
60
- );
61
- }
62
- }
63
-
64
- export class SlotContentValidationError extends Error {
65
- constructor(slotName: string, message: string) {
66
- super(`slot "${slotName}" content failed validation: ${message}`);
67
- this.name = "SlotContentValidationError";
68
- }
69
- }
70
-
71
- export function slotHasContent(items: unknown[]): boolean {
72
- return items.some(
73
- (item) =>
74
- isBlock(item) ||
75
- (typeof item === "string" && item.trim().length > 0),
76
- );
77
- }