@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
package/README.md
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# @headroom-cms/api
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for building sites with [Headroom CMS](https://github.com/headroom-cms). Provides a type-safe API client, block renderers for React and Astro, and an Astro content loader integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @headroom-cms/api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependencies** (optional — install only what you use):
|
|
12
|
+
|
|
13
|
+
| Peer | When needed |
|
|
14
|
+
|------|-------------|
|
|
15
|
+
| `react` + `react-dom` >= 18 | React block rendering (`@headroom-cms/api/react`) |
|
|
16
|
+
| `astro` >= 5 | Astro content loader + dev refresh (`@headroom-cms/api/astro`) |
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { HeadroomClient } from "@headroom-cms/api";
|
|
22
|
+
|
|
23
|
+
const client = new HeadroomClient({
|
|
24
|
+
apiUrl: "https://api.example.com",
|
|
25
|
+
site: "mysite.com",
|
|
26
|
+
apiKey: "headroom_xxxxx",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const { items } = await client.listContent("posts");
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API Client
|
|
33
|
+
|
|
34
|
+
### Configuration
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
interface HeadroomConfig {
|
|
38
|
+
apiUrl: string; // Base API URL
|
|
39
|
+
site: string; // Site host identifier
|
|
40
|
+
apiKey: string; // Public API key
|
|
41
|
+
cdnUrl?: string; // CDN base URL for media
|
|
42
|
+
imageSigningSecret?: string; // HMAC secret for image transforms
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Methods
|
|
47
|
+
|
|
48
|
+
| Method | Returns | Description |
|
|
49
|
+
|--------|---------|-------------|
|
|
50
|
+
| `listContent(collection, opts?)` | `ContentListResult` | List published content with pagination and filtering |
|
|
51
|
+
| `getContent(contentId)` | `ContentItem` | Get a single content item with body and relationships |
|
|
52
|
+
| `getContentBySlug(collection, slug)` | `ContentItem \| undefined` | Look up content by slug (returns `undefined` on 404) |
|
|
53
|
+
| `getSingleton(collection)` | `ContentItem` | Get singleton content (e.g. site settings) |
|
|
54
|
+
| `getBatchContent(ids)` | `BatchContentResult` | Fetch up to 50 items with bodies in one request |
|
|
55
|
+
| `listCollections()` | `CollectionListResult` | List all collections |
|
|
56
|
+
| `getCollection(name)` | `Collection` | Get collection schema with fields and relationships |
|
|
57
|
+
| `listBlockTypes()` | `BlockTypeListResult` | List block type definitions |
|
|
58
|
+
| `getVersion()` | `number` | Content version (for cache busting) |
|
|
59
|
+
| `mediaUrl(path)` | `string` | Prepend CDN base URL to a stored media path |
|
|
60
|
+
| `transformUrl(path, opts?)` | `string` | Build a signed image transform URL |
|
|
61
|
+
|
|
62
|
+
### Query Options for `listContent`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const { items, cursor, hasMore } = await client.listContent("posts", {
|
|
66
|
+
limit: 10,
|
|
67
|
+
cursor: "...", // Pagination cursor from previous response
|
|
68
|
+
sort: "published_desc", // "published_desc" | "published_asc" | "title_asc" | "title_desc"
|
|
69
|
+
before: 1700000000, // Unix timestamp — only items published before
|
|
70
|
+
after: 1690000000, // Unix timestamp — only items published after
|
|
71
|
+
relatedTo: "01ABC", // Reverse relationship: items pointing to this content ID
|
|
72
|
+
relField: "author", // Filter reverse query to a specific relationship field
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Error Handling
|
|
77
|
+
|
|
78
|
+
API errors throw a `HeadroomError` with `status` and `code` properties:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { HeadroomClient, HeadroomError } from "@headroom-cms/api";
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const post = await client.getContent("nonexistent");
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (e instanceof HeadroomError) {
|
|
87
|
+
console.log(e.status); // 404
|
|
88
|
+
console.log(e.code); // "CONTENT_NOT_FOUND"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Block Rendering
|
|
94
|
+
|
|
95
|
+
Content bodies from Headroom contain an array of [BlockNote](https://www.blocknotejs.org/) blocks. The SDK provides renderers for both Astro and React.
|
|
96
|
+
|
|
97
|
+
### Astro (Zero JS)
|
|
98
|
+
|
|
99
|
+
Import `.astro` components directly from `@headroom-cms/api/blocks/*`. These ship as source files compiled by your Astro build — no client-side JavaScript is emitted.
|
|
100
|
+
|
|
101
|
+
```astro
|
|
102
|
+
---
|
|
103
|
+
import BlockRenderer from "@headroom-cms/api/blocks/BlockRenderer.astro";
|
|
104
|
+
import type { Block, RefsMap } from "@headroom-cms/api";
|
|
105
|
+
|
|
106
|
+
const post = await client.getContentBySlug("posts", slug);
|
|
107
|
+
const blocks = (post.body?.content || []) as Block[];
|
|
108
|
+
const refs = (post._refs || {}) as RefsMap;
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
<BlockRenderer
|
|
112
|
+
blocks={blocks}
|
|
113
|
+
refs={refs}
|
|
114
|
+
resolveContentLink={(ref) => `/${ref.collection}/${ref.slug}`}
|
|
115
|
+
transformImage={(path) => client.transformUrl(path, { width: 1200, format: "webp" })}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Props:**
|
|
120
|
+
|
|
121
|
+
| Prop | Type | Description |
|
|
122
|
+
|------|------|-------------|
|
|
123
|
+
| `blocks` | `Block[]` | Block content array |
|
|
124
|
+
| `cdnUrl` | `string?` | CDN base URL (defaults to `HEADROOM_CDN_URL` env var) |
|
|
125
|
+
| `refs` | `RefsMap?` | Content reference map for resolving `headroom://` links |
|
|
126
|
+
| `resolveContentLink` | `(ref: PublicContentRef) => string` | Custom URL builder for content links |
|
|
127
|
+
| `transformImage` | `(path: string) => string` | Custom image URL transform (e.g. for responsive images) |
|
|
128
|
+
| `class` | `string?` | CSS class for the wrapper `<div>` |
|
|
129
|
+
|
|
130
|
+
**Available components** (importable individually from `@headroom-cms/api/blocks/*`):
|
|
131
|
+
|
|
132
|
+
`BlockRenderer`, `Paragraph`, `Heading`, `Image`, `CodeBlock`, `BulletList`, `NumberedList`, `CheckList`, `Table`, `InlineContent`, `Fallback`
|
|
133
|
+
|
|
134
|
+
### React
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { BlockRenderer } from "@headroom-cms/api/react";
|
|
138
|
+
import "@headroom-cms/api/react/headroom-blocks.css";
|
|
139
|
+
|
|
140
|
+
function PostBody({ blocks, refs }) {
|
|
141
|
+
return (
|
|
142
|
+
<BlockRenderer
|
|
143
|
+
blocks={blocks}
|
|
144
|
+
cdnUrl="https://cdn.example.com"
|
|
145
|
+
refs={refs}
|
|
146
|
+
resolveContentLink={(ref) => `/${ref.collection}/${ref.slug}`}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Props:**
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Description |
|
|
155
|
+
|------|------|-------------|
|
|
156
|
+
| `blocks` | `Block[]` | Block content array |
|
|
157
|
+
| `cdnUrl` | `string?` | CDN base URL for media |
|
|
158
|
+
| `refs` | `RefsMap?` | Content reference map |
|
|
159
|
+
| `resolveContentLink` | `(ref: PublicContentRef) => string` | Custom URL builder |
|
|
160
|
+
| `components` | `BlockComponentMap?` | Override or extend block components (see below) |
|
|
161
|
+
| `fallback` | `ComponentType \| null` | Custom fallback for unknown blocks (`null` to suppress) |
|
|
162
|
+
| `className` | `string?` | CSS class for the wrapper `<div>` |
|
|
163
|
+
|
|
164
|
+
**Available block types:** `paragraph`, `heading`, `image`, `codeBlock`, `bulletListItem`, `numberedListItem`, `checkListItem`, `table`
|
|
165
|
+
|
|
166
|
+
## Content Links
|
|
167
|
+
|
|
168
|
+
Rich text can contain `headroom://content/{collection}/{contentId}` links that reference other content. The `_refs` map returned with each content item resolves these to metadata:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const post = await client.getContent("01ABC");
|
|
172
|
+
|
|
173
|
+
// post._refs = {
|
|
174
|
+
// "01DEF": { contentId: "01DEF", collection: "posts", slug: "hello-world", title: "Hello World", published: true }
|
|
175
|
+
// }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The block renderer resolves these links automatically:
|
|
179
|
+
- **Default**: `headroom://content/posts/01DEF` → `/{collection}/{slug}` (i.e. `/posts/hello-world`)
|
|
180
|
+
- **Custom resolver**: Pass `resolveContentLink` to map to your site's URL structure
|
|
181
|
+
- **Broken links**: Unpublished or missing references render as `#`
|
|
182
|
+
|
|
183
|
+
You can also resolve links manually:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import type { PublicContentRef } from "@headroom-cms/api";
|
|
187
|
+
|
|
188
|
+
function resolveContentLink(ref: PublicContentRef): string {
|
|
189
|
+
if (!ref.published) return "#";
|
|
190
|
+
switch (ref.collection) {
|
|
191
|
+
case "posts": return `/blog/${ref.slug}`;
|
|
192
|
+
case "projects": return `/projects/${ref.slug}`;
|
|
193
|
+
default: return `/${ref.slug}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Custom Block Components
|
|
199
|
+
|
|
200
|
+
### React
|
|
201
|
+
|
|
202
|
+
Pass a `components` map to override built-in blocks or render custom block types:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { BlockRenderer } from "@headroom-cms/api/react";
|
|
206
|
+
import type { BlockComponentProps } from "@headroom-cms/api/react";
|
|
207
|
+
|
|
208
|
+
function CallToAction({ block }: BlockComponentProps) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="cta-banner">
|
|
211
|
+
<p>{block.props?.text as string}</p>
|
|
212
|
+
<a href={block.props?.url as string}>Learn more</a>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
<BlockRenderer
|
|
218
|
+
blocks={blocks}
|
|
219
|
+
components={{ callToAction: CallToAction }}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Astro
|
|
224
|
+
|
|
225
|
+
For custom Astro blocks, create your own wrapper around the individual block components. Import and render the built-in components alongside your custom ones:
|
|
226
|
+
|
|
227
|
+
```astro
|
|
228
|
+
---
|
|
229
|
+
import Paragraph from "@headroom-cms/api/blocks/Paragraph.astro";
|
|
230
|
+
import Heading from "@headroom-cms/api/blocks/Heading.astro";
|
|
231
|
+
import Image from "@headroom-cms/api/blocks/Image.astro";
|
|
232
|
+
// ... other built-in imports
|
|
233
|
+
import MyCustomBlock from "../components/MyCustomBlock.astro";
|
|
234
|
+
|
|
235
|
+
const { blocks, refs, resolveContentLink } = Astro.props;
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
{blocks.map((block) => {
|
|
239
|
+
if (block.type === "myCustomBlock") return <MyCustomBlock block={block} />;
|
|
240
|
+
if (block.type === "paragraph") return <Paragraph block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
241
|
+
if (block.type === "heading") return <Heading block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
242
|
+
if (block.type === "image") return <Image block={block} />;
|
|
243
|
+
// ... handle remaining types
|
|
244
|
+
})}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Relationships
|
|
248
|
+
|
|
249
|
+
Collections can define relationships to other collections. These are populated on single-content responses:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Forward relationships (e.g. a project's "artists")
|
|
253
|
+
const project = await client.getContent("01ABC");
|
|
254
|
+
const artists = project.relationships?.artists; // ContentRef[]
|
|
255
|
+
|
|
256
|
+
// Reverse query: find all projects for an artist
|
|
257
|
+
const { items } = await client.listContent("projects", {
|
|
258
|
+
relatedTo: "01ARTIST",
|
|
259
|
+
relField: "artists",
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Media URLs
|
|
264
|
+
|
|
265
|
+
Media paths in content responses (block image URLs, cover images, field values) are stored as relative paths like `/media/{site}/{mediaId}/original.jpg`.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const client = new HeadroomClient({
|
|
269
|
+
apiUrl: "https://api.example.com",
|
|
270
|
+
site: "mysite.com",
|
|
271
|
+
apiKey: "headroom_xxxxx",
|
|
272
|
+
cdnUrl: "https://d123.cloudfront.net",
|
|
273
|
+
imageSigningSecret: "your-secret",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Full CDN URL for the original
|
|
277
|
+
client.mediaUrl(post.coverUrl);
|
|
278
|
+
// → "https://d123.cloudfront.net/media/mysite.com/01ABC/original.jpg"
|
|
279
|
+
|
|
280
|
+
// Signed transform URL (resized, converted to webp)
|
|
281
|
+
client.transformUrl(post.coverUrl, { width: 800, format: "webp" });
|
|
282
|
+
// → "https://d123.cloudfront.net/img/mysite.com/01ABC/original.jpg?format=webp&w=800&sig=abc123..."
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Transform Options
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
interface TransformOptions {
|
|
289
|
+
width?: number; // Target width in pixels
|
|
290
|
+
height?: number; // Target height in pixels
|
|
291
|
+
fit?: "cover" | "contain" | "fill" | "inside" | "outside";
|
|
292
|
+
format?: "webp" | "avif" | "jpeg" | "png"; // Output format
|
|
293
|
+
quality?: number; // 1-100
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Transforms require `imageSigningSecret` in the client config. Without it, `transformUrl()` falls back to `mediaUrl()`.
|
|
298
|
+
|
|
299
|
+
## Astro Integration
|
|
300
|
+
|
|
301
|
+
### Content Loader
|
|
302
|
+
|
|
303
|
+
Use `headroomLoader()` to load Headroom content into Astro's content layer:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// src/content.config.ts
|
|
307
|
+
import { defineCollection } from "astro:content";
|
|
308
|
+
import { headroomLoader } from "@headroom-cms/api/astro";
|
|
309
|
+
|
|
310
|
+
export const collections = {
|
|
311
|
+
posts: defineCollection({
|
|
312
|
+
loader: headroomLoader({ collection: "posts" }),
|
|
313
|
+
}),
|
|
314
|
+
pages: defineCollection({
|
|
315
|
+
loader: headroomLoader({ collection: "pages", bodies: true }),
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Options:**
|
|
321
|
+
|
|
322
|
+
| Option | Type | Default | Description |
|
|
323
|
+
|--------|------|---------|-------------|
|
|
324
|
+
| `collection` | `string` | — | Headroom collection name |
|
|
325
|
+
| `bodies` | `boolean` | `false` | Fetch full content bodies (not just metadata) |
|
|
326
|
+
| `config` | `HeadroomConfig?` | from env | Override client config |
|
|
327
|
+
| `schema` | `ZodType?` | — | Zod schema for type-safe data access |
|
|
328
|
+
|
|
329
|
+
The loader reads config from environment variables by default:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
HEADROOM_API_URL=https://api.example.com
|
|
333
|
+
HEADROOM_SITE=mysite.com
|
|
334
|
+
HEADROOM_API_KEY=headroom_xxxxx
|
|
335
|
+
HEADROOM_CDN_URL=https://d123.cloudfront.net # optional
|
|
336
|
+
HEADROOM_IMAGE_SIGNING_SECRET=your-secret # optional
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Dev Refresh
|
|
340
|
+
|
|
341
|
+
Add `headroomDevRefresh()` to your Astro config for automatic content reloading during development:
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
// astro.config.mjs
|
|
345
|
+
import { headroomDevRefresh } from "@headroom-cms/api/astro";
|
|
346
|
+
|
|
347
|
+
export default defineConfig({
|
|
348
|
+
integrations: [headroomDevRefresh()],
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Polls the Headroom API for version changes (default: every 5 seconds) and triggers a content sync when content is updated in the admin UI.
|
|
353
|
+
|
|
354
|
+
### Zod Schema Codegen
|
|
355
|
+
|
|
356
|
+
Generate type-safe Zod schemas from your Headroom collection definitions:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { HeadroomClient } from "@headroom-cms/api";
|
|
360
|
+
import { generateZodSchemas } from "@headroom-cms/api/codegen";
|
|
361
|
+
|
|
362
|
+
const client = new HeadroomClient({ /* ... */ });
|
|
363
|
+
const code = await generateZodSchemas(client);
|
|
364
|
+
// Write `code` to a file (e.g. src/lib/schemas.ts)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
This generates a TypeScript file with Zod schemas for each collection, ready to pass to `headroomLoader({ schema })`. See the [sample site](../sample-site/) for a working example with a `generate-schemas.sh` script.
|
|
368
|
+
|
|
369
|
+
## Styling
|
|
370
|
+
|
|
371
|
+
### React
|
|
372
|
+
|
|
373
|
+
Import the default stylesheet:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import "@headroom-cms/api/react/headroom-blocks.css";
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Styles use low-specificity `:where()` selectors, making them easy to override. Customize via CSS custom properties:
|
|
380
|
+
|
|
381
|
+
| Property | Default | Used by |
|
|
382
|
+
|----------|---------|---------|
|
|
383
|
+
| `--hr-code-bg` | `#f3f4f6` | Inline code background |
|
|
384
|
+
| `--hr-link-color` | `#2563eb` | Link color |
|
|
385
|
+
| `--hr-image-radius` | `0.5rem` | Image border radius |
|
|
386
|
+
| `--hr-caption-color` | `#6b7280` | Image caption color |
|
|
387
|
+
| `--hr-code-block-bg` | `#1e1e1e` | Code block background |
|
|
388
|
+
| `--hr-code-block-color` | `#d4d4d4` | Code block text color |
|
|
389
|
+
| `--hr-accent` | `#2563eb` | Checkbox accent color |
|
|
390
|
+
| `--hr-table-header-bg` | `#f9fafb` | Table header background |
|
|
391
|
+
|
|
392
|
+
### Astro
|
|
393
|
+
|
|
394
|
+
Astro block components render semantic HTML with no built-in styles. Use Tailwind or your own CSS to style the output. The components use standard HTML elements (`<p>`, `<h1>`–`<h6>`, `<ul>`, `<ol>`, `<figure>`, `<table>`, etc.) that work naturally with Tailwind's `prose` class.
|
|
395
|
+
|
|
396
|
+
## TypeScript Types
|
|
397
|
+
|
|
398
|
+
All types are exported from the main entry point:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import type {
|
|
402
|
+
// Config
|
|
403
|
+
HeadroomConfig,
|
|
404
|
+
TransformOptions,
|
|
405
|
+
|
|
406
|
+
// Content
|
|
407
|
+
ContentItem,
|
|
408
|
+
ContentMetadata,
|
|
409
|
+
ContentListResult,
|
|
410
|
+
BatchContentResult,
|
|
411
|
+
|
|
412
|
+
// Blocks
|
|
413
|
+
Block,
|
|
414
|
+
InlineContent,
|
|
415
|
+
TextContent,
|
|
416
|
+
LinkContent,
|
|
417
|
+
TextStyles,
|
|
418
|
+
TableContent,
|
|
419
|
+
TableRow,
|
|
420
|
+
|
|
421
|
+
// References
|
|
422
|
+
ContentRef,
|
|
423
|
+
PublicContentRef,
|
|
424
|
+
RefsMap,
|
|
425
|
+
|
|
426
|
+
// Collections
|
|
427
|
+
Collection,
|
|
428
|
+
CollectionSummary,
|
|
429
|
+
CollectionListResult,
|
|
430
|
+
FieldDef,
|
|
431
|
+
RelationshipDef,
|
|
432
|
+
|
|
433
|
+
// Block Types
|
|
434
|
+
BlockTypeDef,
|
|
435
|
+
BlockTypeListResult,
|
|
436
|
+
} from "@headroom-cms/api";
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
React-specific types:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import type { BlockRendererProps, BlockComponentProps, BlockComponentMap } from "@headroom-cms/api/react";
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Building & Publishing
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
pnpm build # Build all entry points (ESM + CJS + types)
|
|
449
|
+
pnpm test # Run tests
|
|
450
|
+
pnpm test:watch # Run tests in watch mode
|
|
451
|
+
pnpm typecheck # TypeScript type checking
|
|
452
|
+
pnpm dev # Watch mode build
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Package Entry Points
|
|
456
|
+
|
|
457
|
+
| Import path | Format | Description |
|
|
458
|
+
|-------------|--------|-------------|
|
|
459
|
+
| `@headroom-cms/api` | ESM + CJS | API client and types |
|
|
460
|
+
| `@headroom-cms/api/react` | ESM + CJS | React block renderer components |
|
|
461
|
+
| `@headroom-cms/api/react/headroom-blocks.css` | CSS | Default block styles |
|
|
462
|
+
| `@headroom-cms/api/blocks/*` | Astro source | Astro block components (compiled by consumer) |
|
|
463
|
+
| `@headroom-cms/api/astro` | ESM | Astro content loader + dev refresh |
|
|
464
|
+
| `@headroom-cms/api/codegen` | ESM + CJS | Zod schema generation |
|
|
465
|
+
|
|
466
|
+
### npm Publish
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
pnpm build
|
|
470
|
+
npm publish --access public
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The `files` field in `package.json` includes only `dist/` and `blocks/` directories.
|
|
474
|
+
|
|
475
|
+
## License
|
|
476
|
+
|
|
477
|
+
PolyForm Noncommercial 1.0.0
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block, RefsMap, PublicContentRef } from "../src/types";
|
|
3
|
+
import Paragraph from "./Paragraph.astro";
|
|
4
|
+
import Heading from "./Heading.astro";
|
|
5
|
+
import Image from "./Image.astro";
|
|
6
|
+
import CodeBlock from "./CodeBlock.astro";
|
|
7
|
+
import BulletList from "./BulletList.astro";
|
|
8
|
+
import NumberedList from "./NumberedList.astro";
|
|
9
|
+
import CheckList from "./CheckList.astro";
|
|
10
|
+
import Table from "./Table.astro";
|
|
11
|
+
import Fallback from "./Fallback.astro";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
blocks: Block[];
|
|
15
|
+
cdnUrl?: string;
|
|
16
|
+
refs?: RefsMap;
|
|
17
|
+
resolveContentLink?: (ref: PublicContentRef) => string;
|
|
18
|
+
transformImage?: (path: string) => string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { blocks, cdnUrl = import.meta.env.HEADROOM_CDN_URL, refs, resolveContentLink, transformImage, class: className } = Astro.props;
|
|
23
|
+
|
|
24
|
+
type RenderItem = { listType?: string; block?: Block; items?: Block[] };
|
|
25
|
+
const LIST_TYPE_MAP: Record<string, string> = {
|
|
26
|
+
bulletListItem: "bulletList",
|
|
27
|
+
numberedListItem: "numberedList",
|
|
28
|
+
checkListItem: "checkList",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderItems: RenderItem[] = [];
|
|
32
|
+
for (const block of blocks) {
|
|
33
|
+
const listType = LIST_TYPE_MAP[block.type];
|
|
34
|
+
if (listType) {
|
|
35
|
+
const last = renderItems[renderItems.length - 1];
|
|
36
|
+
if (last?.listType === listType) {
|
|
37
|
+
last.items!.push(block);
|
|
38
|
+
} else {
|
|
39
|
+
renderItems.push({ listType, items: [block] });
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
renderItems.push({ block });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<div class={className}>
|
|
48
|
+
{renderItems.map((item) => {
|
|
49
|
+
if (item.listType === "bulletList") return <BulletList items={item.items!} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
50
|
+
if (item.listType === "numberedList") return <NumberedList items={item.items!} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
51
|
+
if (item.listType === "checkList") return <CheckList items={item.items!} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
52
|
+
|
|
53
|
+
const block = item.block!;
|
|
54
|
+
if (block.type === "paragraph") return <Paragraph block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
55
|
+
if (block.type === "heading") return <Heading block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
56
|
+
if (block.type === "image") return <Image block={block} cdnUrl={cdnUrl} transformImage={transformImage} />;
|
|
57
|
+
if (block.type === "codeBlock") return <CodeBlock block={block} />;
|
|
58
|
+
if (block.type === "table") return <Table block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
59
|
+
return <Fallback block={block} refs={refs} resolveContentLink={resolveContentLink} />;
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
@@ -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
|
+
<ul>
|
|
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
|
+
</ul>
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
<ul class="list-none pl-0">
|
|
15
|
+
{items.map((item) => {
|
|
16
|
+
const checked = item.props?.checked as boolean;
|
|
17
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
18
|
+
return (
|
|
19
|
+
<li class="flex items-start gap-2">
|
|
20
|
+
<span class={`mt-1 inline-block w-4 h-4 rounded border flex-shrink-0 ${checked ? "bg-primary border-primary text-white" : "border-gray-300"}`}>
|
|
21
|
+
{checked && <svg class="w-4 h-4" viewBox="0 0 16 16" fill="none"><path d="M4 8l3 3 5-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>}
|
|
22
|
+
</span>
|
|
23
|
+
<span class={checked ? "line-through text-text-muted" : ""}><InlineContent content={content} refs={refs} resolveContentLink={resolveContentLink} /></span>
|
|
24
|
+
</li>
|
|
25
|
+
);
|
|
26
|
+
})}
|
|
27
|
+
</ul>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block } from "../src/types";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
block: Block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { block } = Astro.props;
|
|
9
|
+
const language = (block.props?.language as string) || "";
|
|
10
|
+
const content = Array.isArray(block.content) ? block.content : [];
|
|
11
|
+
const code = content.map((c) => ("text" in c ? c.text : "")).join("");
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<pre><code class={language ? `language-${language}` : ""}>{code}</code></pre>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block, 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 content = Array.isArray(block.content)
|
|
13
|
+
? (block.content as InlineContentType[])
|
|
14
|
+
: [];
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div class="my-4 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
|
18
|
+
<span class="text-xs text-gray-400 uppercase tracking-wide">{block.type}</span>
|
|
19
|
+
{content.length > 0 && (
|
|
20
|
+
<p class="mt-2">
|
|
21
|
+
<InlineContent content={content} refs={refs} resolveContentLink={resolveContentLink} />
|
|
22
|
+
</p>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
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 level = (block.props?.level as number) || 1;
|
|
13
|
+
const Tag = `h${Math.min(level, 6)}` as any;
|
|
14
|
+
const content = Array.isArray(block.content) ? block.content : [];
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<Tag><InlineContent content={content} refs={refs} resolveContentLink={resolveContentLink} /></Tag>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Block } from "../src/types";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
block: Block;
|
|
6
|
+
cdnUrl?: string;
|
|
7
|
+
transformImage?: (path: string) => string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { block, cdnUrl = import.meta.env.HEADROOM_CDN_URL, transformImage } = Astro.props;
|
|
11
|
+
const storedPath = block.props?.url as string | undefined;
|
|
12
|
+
|
|
13
|
+
let src = "";
|
|
14
|
+
if (storedPath) {
|
|
15
|
+
if (transformImage) {
|
|
16
|
+
src = transformImage(storedPath);
|
|
17
|
+
} else {
|
|
18
|
+
src = cdnUrl ? `${cdnUrl}${storedPath}` : storedPath;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const alt = (block.props?.alt as string) || (block.props?.caption as string) || "";
|
|
23
|
+
const caption = (block.props?.caption as string) || "";
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
{src && (
|
|
27
|
+
<figure class="my-4">
|
|
28
|
+
<img src={src} alt={alt} loading="lazy" decoding="async" class="rounded-lg max-w-full h-auto" />
|
|
29
|
+
{caption && <figcaption class="text-sm text-center text-gray-500 mt-2">{caption}</figcaption>}
|
|
30
|
+
</figure>
|
|
31
|
+
)}
|