@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 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
+ )}