@headroom-cms/api 0.1.1
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.
- package/README.md +477 -0
- package/blocks/BlockRenderer.astro +61 -0
- package/blocks/BulletList.astro +19 -0
- package/blocks/CheckList.astro +27 -0
- package/blocks/CodeBlock.astro +14 -0
- package/blocks/Fallback.astro +24 -0
- package/blocks/Heading.astro +17 -0
- package/blocks/Image.astro +31 -0
- package/blocks/InlineContent.astro +47 -0
- package/blocks/NumberedList.astro +19 -0
- package/blocks/Paragraph.astro +15 -0
- package/blocks/Table.astro +29 -0
- package/dist/astro.d.ts +49 -0
- package/dist/astro.js +325 -0
- package/dist/codegen.cjs +172 -0
- package/dist/codegen.d.cts +49 -0
- package/dist/codegen.d.ts +49 -0
- package/dist/codegen.js +144 -0
- package/dist/headroom-blocks.css +96 -0
- package/dist/index.cjs +202 -0
- package/dist/index.d.cts +227 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.js +174 -0
- package/dist/react.cjs +323 -0
- package/dist/react.d.cts +105 -0
- package/dist/react.d.ts +105 -0
- package/dist/react.js +275 -0
- package/package.json +72 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { InlineContent as InlineContentType, RefsMap, PublicContentRef } from "../src/types";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
content: InlineContentType[] | undefined | null;
|
|
6
|
+
refs?: RefsMap;
|
|
7
|
+
resolveContentLink?: (ref: PublicContentRef) => string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { content, refs, resolveContentLink } = Astro.props;
|
|
11
|
+
|
|
12
|
+
function resolveHref(href: string): string {
|
|
13
|
+
if (!href.startsWith("headroom://content/")) return href;
|
|
14
|
+
const match = href.match(/^headroom:\/\/content\/([^/]+)\/([A-Z0-9]+)$/);
|
|
15
|
+
if (!match) return "#";
|
|
16
|
+
const [, , contentId] = match;
|
|
17
|
+
const ref = refs?.[contentId];
|
|
18
|
+
if (!ref) return "#";
|
|
19
|
+
if (resolveContentLink) return resolveContentLink(ref);
|
|
20
|
+
if (!ref.published) return "#";
|
|
21
|
+
return `/${ref.collection}/${ref.slug}`;
|
|
22
|
+
}
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{content?.map((item) => {
|
|
26
|
+
if (item.type === "link") {
|
|
27
|
+
const resolvedHref = resolveHref(item.href);
|
|
28
|
+
return (
|
|
29
|
+
<a href={resolvedHref} class="text-primary underline hover:text-primary/80">
|
|
30
|
+
<Astro.self content={item.content} refs={refs} resolveContentLink={resolveContentLink} />
|
|
31
|
+
</a>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const s = item.styles;
|
|
36
|
+
const classes = [
|
|
37
|
+
s?.bold && "font-bold",
|
|
38
|
+
s?.italic && "italic",
|
|
39
|
+
s?.underline && "underline",
|
|
40
|
+
s?.strikethrough && "line-through",
|
|
41
|
+
s?.code && "font-mono bg-surface-alt px-1 py-0.5 rounded text-sm",
|
|
42
|
+
].filter(Boolean).join(" ");
|
|
43
|
+
|
|
44
|
+
return classes
|
|
45
|
+
? <span class={classes}>{item.text}</span>
|
|
46
|
+
: <Fragment>{item.text}</Fragment>;
|
|
47
|
+
})}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block, RefsMap, PublicContentRef } from "../src/types";
|
|
3
|
+
import InlineContent from "./InlineContent.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: Block[];
|
|
7
|
+
refs?: RefsMap;
|
|
8
|
+
resolveContentLink?: (ref: PublicContentRef) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { items, refs, resolveContentLink } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<ol>
|
|
15
|
+
{items.map((item) => {
|
|
16
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
17
|
+
return <li><InlineContent content={content} refs={refs} resolveContentLink={resolveContentLink} /></li>;
|
|
18
|
+
})}
|
|
19
|
+
</ol>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block, RefsMap, PublicContentRef } from "../src/types";
|
|
3
|
+
import InlineContent from "./InlineContent.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
block: Block;
|
|
7
|
+
refs?: RefsMap;
|
|
8
|
+
resolveContentLink?: (ref: PublicContentRef) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { block, refs, resolveContentLink } = Astro.props;
|
|
12
|
+
const content = Array.isArray(block.content) ? block.content : [];
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<p><InlineContent content={content} refs={refs} resolveContentLink={resolveContentLink} /></p>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block, TableContent, InlineContent as InlineContentType, RefsMap, PublicContentRef } from "../src/types";
|
|
3
|
+
import InlineContent from "./InlineContent.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
block: Block;
|
|
7
|
+
refs?: RefsMap;
|
|
8
|
+
resolveContentLink?: (ref: PublicContentRef) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { block, refs, resolveContentLink } = Astro.props;
|
|
12
|
+
const tableContent = block.content as TableContent | undefined;
|
|
13
|
+
const rows = tableContent?.rows || [];
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{rows.length > 0 && (
|
|
17
|
+
<table>
|
|
18
|
+
<tbody>
|
|
19
|
+
{rows.map((row, rowIndex) => (
|
|
20
|
+
<tr>
|
|
21
|
+
{row.cells.map((cell) => {
|
|
22
|
+
const Tag = rowIndex === 0 ? "th" : "td";
|
|
23
|
+
return <Tag><InlineContent content={cell as InlineContentType[]} refs={refs} resolveContentLink={resolveContentLink} /></Tag>;
|
|
24
|
+
})}
|
|
25
|
+
</tr>
|
|
26
|
+
))}
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
)}
|
package/dist/astro.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Loader } from 'astro/loaders';
|
|
2
|
+
import { ZodTypeAny } from 'astro/zod';
|
|
3
|
+
import { AstroIntegration } from 'astro';
|
|
4
|
+
|
|
5
|
+
interface HeadroomConfig {
|
|
6
|
+
/** Base URL of the Headroom API (e.g. "https://api.example.com") */
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
/** Site host identifier (e.g. "mysite.com") */
|
|
9
|
+
site: string;
|
|
10
|
+
/** Public API key for X-Headroom-Key header */
|
|
11
|
+
apiKey: string;
|
|
12
|
+
/** CDN base URL for media (e.g. "https://d123.cloudfront.net"). Required for mediaUrl(). */
|
|
13
|
+
cdnUrl?: string;
|
|
14
|
+
/** HMAC secret for signing image transform URLs. Required for transformUrl(). */
|
|
15
|
+
imageSigningSecret?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface HeadroomLoaderOptions {
|
|
19
|
+
/** Collection name to load */
|
|
20
|
+
collection: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to fetch full content bodies (default: false).
|
|
23
|
+
* When false, only metadata is stored (faster, less data).
|
|
24
|
+
* When true, bodies are batch-fetched and stored alongside metadata.
|
|
25
|
+
*/
|
|
26
|
+
bodies?: boolean;
|
|
27
|
+
/** Client config. If omitted, reads from env vars. */
|
|
28
|
+
config?: HeadroomConfig;
|
|
29
|
+
/** Zod schema for content data. Enables type-safe access via `entry.data`. */
|
|
30
|
+
schema?: ZodTypeAny;
|
|
31
|
+
}
|
|
32
|
+
declare function headroomLoader(opts: HeadroomLoaderOptions): Loader;
|
|
33
|
+
|
|
34
|
+
interface HeadroomRefreshOptions {
|
|
35
|
+
/** Poll interval in milliseconds (default: 5000) */
|
|
36
|
+
interval?: number;
|
|
37
|
+
/** Client config. If omitted, reads from env vars. */
|
|
38
|
+
config?: HeadroomConfig;
|
|
39
|
+
}
|
|
40
|
+
declare function headroomDevRefresh(opts?: HeadroomRefreshOptions): AstroIntegration;
|
|
41
|
+
|
|
42
|
+
declare function getEnv(key: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Build config from env vars. In Astro integration hooks, process.env may
|
|
45
|
+
* not contain .env file values, so we also read .env directly from cwd.
|
|
46
|
+
*/
|
|
47
|
+
declare function configFromEnv(): HeadroomConfig;
|
|
48
|
+
|
|
49
|
+
export { type HeadroomLoaderOptions, type HeadroomRefreshOptions, configFromEnv, getEnv, headroomDevRefresh, headroomLoader };
|
package/dist/astro.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
var HeadroomError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
constructor(status, code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "HeadroomError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var HeadroomClient = class {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = {
|
|
17
|
+
...config,
|
|
18
|
+
apiUrl: config.apiUrl.replace(/\/+$/, ""),
|
|
19
|
+
cdnUrl: config.cdnUrl?.replace(/\/+$/, "")
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Build the full URL for a public API path */
|
|
23
|
+
url(path) {
|
|
24
|
+
return `${this.config.apiUrl}/v1/${this.config.site}${path}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Construct a full CDN URL from a stored media path.
|
|
28
|
+
* Media paths are stored as `/media/{site}/{mediaId}/original{ext}` in
|
|
29
|
+
* content responses (block props, coverUrl, field values).
|
|
30
|
+
*
|
|
31
|
+
* client.mediaUrl("/media/myblog.com/01ABC/original.jpg")
|
|
32
|
+
* → "https://d123.cloudfront.net/media/myblog.com/01ABC/original.jpg"
|
|
33
|
+
*
|
|
34
|
+
* Returns the path as-is if no cdnUrl is configured (useful for dev).
|
|
35
|
+
*/
|
|
36
|
+
mediaUrl(path) {
|
|
37
|
+
if (!path) return "";
|
|
38
|
+
return this.config.cdnUrl ? `${this.config.cdnUrl}${path}` : path;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a signed image transform URL from a stored media path.
|
|
42
|
+
* Converts the `/media/...` path to `/img/...`, appends transform
|
|
43
|
+
* parameters, and signs with HMAC-SHA256 (matching the Go imagesign).
|
|
44
|
+
*
|
|
45
|
+
* Falls back to `mediaUrl()` when no signing secret is configured
|
|
46
|
+
* or when no transform options are provided.
|
|
47
|
+
*
|
|
48
|
+
* client.transformUrl("/media/myblog.com/01ABC/original.jpg", { width: 800, format: "webp" })
|
|
49
|
+
* → "https://cdn.example.com/img/myblog.com/01ABC/original.jpg?format=webp&w=800&sig=abc123..."
|
|
50
|
+
*/
|
|
51
|
+
transformUrl(path, opts) {
|
|
52
|
+
if (!path) return "";
|
|
53
|
+
if (!this.config.imageSigningSecret || !opts || Object.keys(opts).length === 0) {
|
|
54
|
+
return this.mediaUrl(path);
|
|
55
|
+
}
|
|
56
|
+
const imgPath = path.replace(/^\/media\//, "/img/");
|
|
57
|
+
const params = new URLSearchParams();
|
|
58
|
+
if (opts.width) params.set("w", String(opts.width));
|
|
59
|
+
if (opts.height) params.set("h", String(opts.height));
|
|
60
|
+
if (opts.fit) params.set("fit", opts.fit);
|
|
61
|
+
if (opts.format) params.set("format", opts.format);
|
|
62
|
+
if (opts.quality) params.set("q", String(opts.quality));
|
|
63
|
+
params.sort();
|
|
64
|
+
const encoded = params.toString();
|
|
65
|
+
const canonical = encoded ? `${imgPath}?${encoded}` : imgPath;
|
|
66
|
+
const mac = createHmac("sha256", this.config.imageSigningSecret).update(canonical).digest("hex").slice(0, 32);
|
|
67
|
+
params.set("sig", mac);
|
|
68
|
+
const cdnBase = this.config.cdnUrl || "";
|
|
69
|
+
return `${cdnBase}${imgPath}?${params.toString()}`;
|
|
70
|
+
}
|
|
71
|
+
async fetch(path, params) {
|
|
72
|
+
const base = this.url(path);
|
|
73
|
+
const url = params?.toString() ? `${base}?${params}` : base;
|
|
74
|
+
const res = await fetch(url, {
|
|
75
|
+
headers: { "X-Headroom-Key": this.config.apiKey }
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
let code = "UNKNOWN";
|
|
79
|
+
let message = `HTTP ${res.status}`;
|
|
80
|
+
try {
|
|
81
|
+
const body = await res.json();
|
|
82
|
+
code = body.code || code;
|
|
83
|
+
message = body.error || message;
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
throw new HeadroomError(res.status, code, message);
|
|
87
|
+
}
|
|
88
|
+
return res.json();
|
|
89
|
+
}
|
|
90
|
+
async fetchPost(path, body) {
|
|
91
|
+
const res = await fetch(this.url(path), {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"X-Headroom-Key": this.config.apiKey,
|
|
95
|
+
"Content-Type": "application/json"
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(body)
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
let code = "UNKNOWN";
|
|
101
|
+
let message = `HTTP ${res.status}`;
|
|
102
|
+
try {
|
|
103
|
+
const errBody = await res.json();
|
|
104
|
+
code = errBody.code || code;
|
|
105
|
+
message = errBody.error || message;
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
throw new HeadroomError(res.status, code, message);
|
|
109
|
+
}
|
|
110
|
+
return res.json();
|
|
111
|
+
}
|
|
112
|
+
// --- Content ---
|
|
113
|
+
async listContent(collection, opts) {
|
|
114
|
+
const params = new URLSearchParams({ collection });
|
|
115
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
116
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
117
|
+
if (opts?.before) params.set("before", String(opts.before));
|
|
118
|
+
if (opts?.after) params.set("after", String(opts.after));
|
|
119
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
120
|
+
if (opts?.relatedTo) params.set("relatedTo", opts.relatedTo);
|
|
121
|
+
if (opts?.relField) params.set("relField", opts.relField);
|
|
122
|
+
return this.fetch("/content", params);
|
|
123
|
+
}
|
|
124
|
+
async getContent(contentId) {
|
|
125
|
+
return this.fetch(`/content/${contentId}`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fetch a single content item by slug. Uses a dedicated server-side
|
|
129
|
+
* endpoint for efficient lookup (no client-side pagination).
|
|
130
|
+
* Returns undefined if no published content matches the slug.
|
|
131
|
+
*/
|
|
132
|
+
async getContentBySlug(collection, slug) {
|
|
133
|
+
try {
|
|
134
|
+
return await this.fetch(
|
|
135
|
+
`/content/by-slug/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`
|
|
136
|
+
);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e instanceof HeadroomError && e.status === 404) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async getSingleton(collection) {
|
|
145
|
+
return this.fetch(`/content/singleton/${collection}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch multiple content items with bodies in a single request.
|
|
149
|
+
* Max 50 IDs per call. Items not found or unpublished are listed in `missing`.
|
|
150
|
+
*/
|
|
151
|
+
async getBatchContent(ids) {
|
|
152
|
+
return this.fetchPost("/content/batch", { ids });
|
|
153
|
+
}
|
|
154
|
+
// --- Collections ---
|
|
155
|
+
async listCollections() {
|
|
156
|
+
return this.fetch("/collections");
|
|
157
|
+
}
|
|
158
|
+
async getCollection(name) {
|
|
159
|
+
return this.fetch(`/collections/${name}`);
|
|
160
|
+
}
|
|
161
|
+
// --- Block Types ---
|
|
162
|
+
async listBlockTypes() {
|
|
163
|
+
return this.fetch("/block-types");
|
|
164
|
+
}
|
|
165
|
+
// --- Version ---
|
|
166
|
+
async getVersion() {
|
|
167
|
+
const result = await this.fetch("/version");
|
|
168
|
+
return result.contentVersion;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/astro/env.ts
|
|
173
|
+
import { readFileSync } from "fs";
|
|
174
|
+
import { resolve } from "path";
|
|
175
|
+
function getEnv(key) {
|
|
176
|
+
if (typeof process !== "undefined" && process.env?.[key]) {
|
|
177
|
+
return process.env[key];
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const env = import.meta.env;
|
|
181
|
+
if (env?.[key]) return env[key];
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
function parseDotenv(filePath) {
|
|
187
|
+
try {
|
|
188
|
+
const content = readFileSync(filePath, "utf-8");
|
|
189
|
+
const env = {};
|
|
190
|
+
for (const line of content.split("\n")) {
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
193
|
+
const eqIdx = trimmed.indexOf("=");
|
|
194
|
+
if (eqIdx === -1) continue;
|
|
195
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
196
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
197
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
198
|
+
value = value.slice(1, -1);
|
|
199
|
+
}
|
|
200
|
+
env[key] = value;
|
|
201
|
+
}
|
|
202
|
+
return env;
|
|
203
|
+
} catch {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function configFromEnv() {
|
|
208
|
+
const dotenv = parseDotenv(resolve(process.cwd(), ".env"));
|
|
209
|
+
const get = (key) => getEnv(key) || dotenv[key];
|
|
210
|
+
return {
|
|
211
|
+
apiUrl: get("HEADROOM_API_URL") || "http://localhost:3000",
|
|
212
|
+
site: get("HEADROOM_SITE") || "",
|
|
213
|
+
apiKey: get("HEADROOM_API_KEY") || "",
|
|
214
|
+
cdnUrl: get("HEADROOM_CDN_URL") || void 0,
|
|
215
|
+
imageSigningSecret: get("HEADROOM_IMAGE_SIGNING_SECRET") || void 0
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/astro/loader.ts
|
|
220
|
+
function headroomLoader(opts) {
|
|
221
|
+
return {
|
|
222
|
+
name: "headroom",
|
|
223
|
+
schema: opts.schema,
|
|
224
|
+
load: async ({ store, meta, generateDigest, parseData }) => {
|
|
225
|
+
const config = opts.config || configFromEnv();
|
|
226
|
+
const client = new HeadroomClient(config);
|
|
227
|
+
try {
|
|
228
|
+
const version = await client.getVersion();
|
|
229
|
+
const lastVersion = meta.get("contentVersion");
|
|
230
|
+
if (lastVersion && String(version) === lastVersion) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
meta.set("contentVersion", String(version));
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
const allMetadata = [];
|
|
237
|
+
let cursor;
|
|
238
|
+
do {
|
|
239
|
+
const result = await client.listContent(opts.collection, {
|
|
240
|
+
limit: 1e3,
|
|
241
|
+
cursor
|
|
242
|
+
});
|
|
243
|
+
allMetadata.push(...result.items);
|
|
244
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
245
|
+
} while (cursor);
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
const storeId = (item) => item.slug || opts.collection;
|
|
248
|
+
if (opts.bodies) {
|
|
249
|
+
for (let i = 0; i < allMetadata.length; i += 50) {
|
|
250
|
+
const chunk = allMetadata.slice(i, i + 50);
|
|
251
|
+
const result = await client.getBatchContent(
|
|
252
|
+
chunk.map((m) => m.contentId)
|
|
253
|
+
);
|
|
254
|
+
for (const item of result.items) {
|
|
255
|
+
const id = storeId(item);
|
|
256
|
+
seen.add(id);
|
|
257
|
+
const raw = item;
|
|
258
|
+
const data = parseData ? await parseData({ id, data: raw }) : raw;
|
|
259
|
+
store.set({
|
|
260
|
+
id,
|
|
261
|
+
data,
|
|
262
|
+
digest: generateDigest(raw)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
for (const item of allMetadata) {
|
|
268
|
+
const id = storeId(item);
|
|
269
|
+
seen.add(id);
|
|
270
|
+
const raw = item;
|
|
271
|
+
const data = parseData ? await parseData({ id, data: raw }) : raw;
|
|
272
|
+
store.set({
|
|
273
|
+
id,
|
|
274
|
+
data,
|
|
275
|
+
digest: generateDigest(raw)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const key of store.keys()) {
|
|
280
|
+
if (!seen.has(key)) {
|
|
281
|
+
store.delete(key);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/astro/refresh.ts
|
|
289
|
+
function headroomDevRefresh(opts) {
|
|
290
|
+
const interval = opts?.interval || 5e3;
|
|
291
|
+
return {
|
|
292
|
+
name: "headroom-dev-refresh",
|
|
293
|
+
hooks: {
|
|
294
|
+
"astro:server:setup": ({ logger, refreshContent }) => {
|
|
295
|
+
const config = opts?.config || configFromEnv();
|
|
296
|
+
const client = new HeadroomClient(config);
|
|
297
|
+
logger.info(
|
|
298
|
+
`Polling ${config.apiUrl}/v1/${config.site}/version every ${interval / 1e3}s`
|
|
299
|
+
);
|
|
300
|
+
let lastVersion = null;
|
|
301
|
+
setInterval(async () => {
|
|
302
|
+
try {
|
|
303
|
+
const version = await client.getVersion();
|
|
304
|
+
if (lastVersion !== null && version !== lastVersion) {
|
|
305
|
+
logger.info(
|
|
306
|
+
`Content version changed (${lastVersion} \u2192 ${version}), refreshing\u2026`
|
|
307
|
+
);
|
|
308
|
+
await refreshContent({ loaders: ["headroom"] });
|
|
309
|
+
}
|
|
310
|
+
lastVersion = version;
|
|
311
|
+
} catch (e) {
|
|
312
|
+
const cause = e instanceof Error && e.cause ? ` (${e.cause})` : "";
|
|
313
|
+
logger.warn(`Version poll error: ${e}${cause}`);
|
|
314
|
+
}
|
|
315
|
+
}, interval);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export {
|
|
321
|
+
configFromEnv,
|
|
322
|
+
getEnv,
|
|
323
|
+
headroomDevRefresh,
|
|
324
|
+
headroomLoader
|
|
325
|
+
};
|
package/dist/codegen.cjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/codegen.ts
|
|
21
|
+
var codegen_exports = {};
|
|
22
|
+
__export(codegen_exports, {
|
|
23
|
+
fieldToZod: () => fieldToZod,
|
|
24
|
+
generateZodSchemas: () => generateZodSchemas
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(codegen_exports);
|
|
27
|
+
|
|
28
|
+
// src/codegen/zod.ts
|
|
29
|
+
function fieldToZod(field) {
|
|
30
|
+
switch (field.type) {
|
|
31
|
+
case "text":
|
|
32
|
+
case "textarea":
|
|
33
|
+
case "rich-text":
|
|
34
|
+
case "slug":
|
|
35
|
+
case "url":
|
|
36
|
+
case "email":
|
|
37
|
+
case "select":
|
|
38
|
+
return "z.string().optional()";
|
|
39
|
+
case "number":
|
|
40
|
+
return "z.number().optional()";
|
|
41
|
+
case "boolean":
|
|
42
|
+
return "z.boolean().optional()";
|
|
43
|
+
case "date":
|
|
44
|
+
case "datetime":
|
|
45
|
+
return "z.number().optional()";
|
|
46
|
+
case "media":
|
|
47
|
+
return "z.string().optional()";
|
|
48
|
+
case "blocks":
|
|
49
|
+
return "z.array(blockSchema).optional()";
|
|
50
|
+
case "multiselect":
|
|
51
|
+
return "z.array(z.string()).optional()";
|
|
52
|
+
case "content": {
|
|
53
|
+
const multiple = field.options?.multiple === true;
|
|
54
|
+
return multiple ? "z.array(z.string()).optional()" : "z.string().optional()";
|
|
55
|
+
}
|
|
56
|
+
case "array": {
|
|
57
|
+
const itemFields = field.options?.itemFields;
|
|
58
|
+
if (itemFields && itemFields.length > 0) {
|
|
59
|
+
const props = itemFields.map((f) => `${f.name}: ${fieldToZod(f)}`).join(", ");
|
|
60
|
+
return `z.array(z.object({ ${props} })).optional()`;
|
|
61
|
+
}
|
|
62
|
+
return "z.array(z.record(z.unknown())).optional()";
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
return "z.unknown().optional()";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var PREAMBLE = `// Auto-generated by @headroom-cms/api \u2014 do not edit
|
|
69
|
+
|
|
70
|
+
`;
|
|
71
|
+
var SHARED_TYPES = `
|
|
72
|
+
/** Block content type (loosely typed \u2014 blocks are rendered dynamically). */
|
|
73
|
+
type Block = {
|
|
74
|
+
id: string;
|
|
75
|
+
type: string;
|
|
76
|
+
props?: Record<string, unknown> | null;
|
|
77
|
+
content?: unknown;
|
|
78
|
+
children?: Block[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Shared metadata fields present on all content items. */
|
|
82
|
+
const contentMeta = {
|
|
83
|
+
contentId: z.string(),
|
|
84
|
+
collection: z.string(),
|
|
85
|
+
slug: z.string(),
|
|
86
|
+
title: z.string(),
|
|
87
|
+
snippet: z.string(),
|
|
88
|
+
publishedAt: z.number(),
|
|
89
|
+
tags: z.array(z.string()),
|
|
90
|
+
coverUrl: z.string().optional(),
|
|
91
|
+
coverMediaId: z.string().optional(),
|
|
92
|
+
lastPublishedAt: z.number().optional(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Block content schema (loosely typed \u2014 blocks are rendered dynamically). */
|
|
96
|
+
const blockSchema: ZodType<Block> = z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
type: z.string(),
|
|
99
|
+
props: z.record(z.unknown()).nullable().optional(),
|
|
100
|
+
content: z.unknown().optional(),
|
|
101
|
+
children: z.lazy(() => z.array(blockSchema)).optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** Content reference schema. */
|
|
105
|
+
const publicContentRefSchema = z.object({
|
|
106
|
+
contentId: z.string(),
|
|
107
|
+
collection: z.string(),
|
|
108
|
+
slug: z.string(),
|
|
109
|
+
title: z.string(),
|
|
110
|
+
published: z.boolean(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const refsMapSchema = z.record(publicContentRefSchema);
|
|
114
|
+
`;
|
|
115
|
+
function collectionToZod(col) {
|
|
116
|
+
const bodyFields = col.fields.filter((f) => f.type !== "blocks");
|
|
117
|
+
const hasBlocks = col.fields.some((f) => f.type === "blocks");
|
|
118
|
+
const hasBodyFields = bodyFields.length > 0 || hasBlocks;
|
|
119
|
+
const lines = [];
|
|
120
|
+
const exportName = camelCase(col.name) + "Schema";
|
|
121
|
+
lines.push(`/** ${col.label}${col.singleton ? " (singleton)" : ""} */`);
|
|
122
|
+
lines.push(`export const ${exportName} = z`);
|
|
123
|
+
if (!hasBodyFields) {
|
|
124
|
+
lines.push(" .object({");
|
|
125
|
+
lines.push(" ...contentMeta,");
|
|
126
|
+
lines.push(" })");
|
|
127
|
+
} else {
|
|
128
|
+
lines.push(" .object({");
|
|
129
|
+
lines.push(" ...contentMeta,");
|
|
130
|
+
lines.push(" body: z");
|
|
131
|
+
lines.push(" .object({");
|
|
132
|
+
if (hasBlocks) {
|
|
133
|
+
lines.push(" content: z.array(blockSchema).optional(),");
|
|
134
|
+
}
|
|
135
|
+
for (const field of bodyFields) {
|
|
136
|
+
lines.push(` ${field.name}: ${fieldToZod(field)},`);
|
|
137
|
+
}
|
|
138
|
+
lines.push(" })");
|
|
139
|
+
lines.push(" .passthrough()");
|
|
140
|
+
lines.push(" .optional(),");
|
|
141
|
+
lines.push(" _refs: refsMapSchema.optional(),");
|
|
142
|
+
lines.push(" })");
|
|
143
|
+
}
|
|
144
|
+
lines.push(" .passthrough();");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
function camelCase(s) {
|
|
148
|
+
return s.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase());
|
|
149
|
+
}
|
|
150
|
+
async function generateZodSchemas(client, opts) {
|
|
151
|
+
const zodImport = opts?.zodImport ?? "astro/zod";
|
|
152
|
+
const { items: summaries } = await client.listCollections();
|
|
153
|
+
const filtered = opts?.collections ? summaries.filter((s) => opts.collections.includes(s.name)) : summaries;
|
|
154
|
+
const collections = await Promise.all(
|
|
155
|
+
filtered.map((s) => client.getCollection(s.name))
|
|
156
|
+
);
|
|
157
|
+
const parts = [];
|
|
158
|
+
parts.push(PREAMBLE);
|
|
159
|
+
parts.push(`import { z, type ZodType } from "${zodImport}";
|
|
160
|
+
`);
|
|
161
|
+
parts.push(SHARED_TYPES);
|
|
162
|
+
for (const col of collections) {
|
|
163
|
+
parts.push("");
|
|
164
|
+
parts.push(collectionToZod(col));
|
|
165
|
+
}
|
|
166
|
+
return parts.join("\n") + "\n";
|
|
167
|
+
}
|
|
168
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
169
|
+
0 && (module.exports = {
|
|
170
|
+
fieldToZod,
|
|
171
|
+
generateZodSchemas
|
|
172
|
+
});
|