@headroom-cms/api
v0.1.7
Published
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.
Readme
@headroom-cms/api
TypeScript SDK for building sites with Headroom CMS. Provides a type-safe API client, block renderers for React and Astro, and an Astro content loader integration.
Installation
npm install @headroom-cms/apiPeer dependencies (optional — install only what you use):
| Peer | When needed |
|------|-------------|
| react + react-dom >= 18 | React block rendering (@headroom-cms/api/react) |
| astro >= 5 | Astro content loader + dev refresh (@headroom-cms/api/astro) |
Quick Start
import { HeadroomClient } from "@headroom-cms/api";
const client = new HeadroomClient({
url: "https://headroom.example.com",
site: "mysite.com",
apiKey: "headroom_xxxxx",
});
const { items } = await client.listContent("posts");API Client
Configuration
interface HeadroomConfig {
url: string; // Headroom CDN URL (used for both API calls and media)
site: string; // Site host identifier
apiKey: string; // Public API key
imageSigningSecret?: string; // HMAC secret for image transforms
}Methods
| Method | Returns | Description |
|--------|---------|-------------|
| listContent(collection, opts?) | ContentListResult | List published content with pagination and filtering |
| getContent(contentId) | ContentItem | Get a single content item with body and relationships |
| getContentBySlug(collection, slug) | ContentItem \| undefined | Look up content by slug (returns undefined on 404) |
| getSingleton(collection) | ContentItem | Get singleton content (e.g. site settings) |
| getBatchContent(ids) | BatchContentResult | Fetch up to 50 items with bodies in one request |
| listCollections() | CollectionListResult | List all collections |
| getCollection(name) | Collection | Get collection schema with fields and relationships |
| listBlockTypes() | BlockTypeListResult | List block type definitions |
| getVersion() | number | Content version (for cache busting) |
| mediaUrl(path) | string | Prepend base URL to a stored media path |
| transformUrl(path, opts?) | string | Build a signed image transform URL |
Query Options for listContent
const { items, cursor, hasMore } = await client.listContent("posts", {
limit: 10,
cursor: "...", // Pagination cursor from previous response
sort: "published_desc", // "published_desc" | "published_asc" | "title_asc" | "title_desc"
before: 1700000000, // Unix timestamp — only items published before
after: 1690000000, // Unix timestamp — only items published after
relatedTo: "01ABC", // Reverse relationship: items pointing to this content ID
relField: "author", // Filter reverse query to a specific relationship field
});Error Handling
API errors throw a HeadroomError with status and code properties:
import { HeadroomClient, HeadroomError } from "@headroom-cms/api";
try {
const post = await client.getContent("nonexistent");
} catch (e) {
if (e instanceof HeadroomError) {
console.log(e.status); // 404
console.log(e.code); // "CONTENT_NOT_FOUND"
}
}Block Rendering
Content bodies from Headroom contain an array of BlockNote blocks. The SDK provides renderers for both Astro and React.
Astro (Zero JS)
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.
---
import BlockRenderer from "@headroom-cms/api/blocks/BlockRenderer.astro";
import type { Block, RefsMap } from "@headroom-cms/api";
const post = await client.getContentBySlug("posts", slug);
const blocks = (post.body?.content || []) as Block[];
const refs = (post._refs || {}) as RefsMap;
---
<BlockRenderer
blocks={blocks}
refs={refs}
resolveContentLink={(ref) => `/${ref.collection}/${ref.slug}`}
transformImage={(path) => client.transformUrl(path, { width: 1200, format: "webp" })}
/>Props:
| Prop | Type | Description |
|------|------|-------------|
| blocks | Block[] | Block content array |
| baseUrl | string? | Base URL for media (defaults to HEADROOM_URL env var) |
| refs | RefsMap? | Content reference map for resolving headroom:// links |
| resolveContentLink | (ref: PublicContentRef) => string | Custom URL builder for content links |
| transformImage | (path: string) => string | Custom image URL transform (e.g. for responsive images) |
| class | string? | CSS class for the wrapper <div> |
Available components (importable individually from @headroom-cms/api/blocks/*):
BlockRenderer, Paragraph, Heading, Image, CodeBlock, BulletList, NumberedList, CheckList, Table, InlineContent, Fallback
React
import { BlockRenderer } from "@headroom-cms/api/react";
import "@headroom-cms/api/react/headroom-blocks.css";
function PostBody({ blocks, refs }) {
return (
<BlockRenderer
blocks={blocks}
baseUrl="https://headroom.example.com"
refs={refs}
resolveContentLink={(ref) => `/${ref.collection}/${ref.slug}`}
/>
);
}Props:
| Prop | Type | Description |
|------|------|-------------|
| blocks | Block[] | Block content array |
| baseUrl | string? | Base URL for media |
| refs | RefsMap? | Content reference map |
| resolveContentLink | (ref: PublicContentRef) => string | Custom URL builder |
| components | BlockComponentMap? | Override or extend block components (see below) |
| fallback | ComponentType \| null | Custom fallback for unknown blocks (null to suppress) |
| className | string? | CSS class for the wrapper <div> |
Available block types: paragraph, heading, image, codeBlock, bulletListItem, numberedListItem, checkListItem, table
Content Links
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:
const post = await client.getContent("01ABC");
// post._refs = {
// "01DEF": { contentId: "01DEF", collection: "posts", slug: "hello-world", title: "Hello World", published: true }
// }The block renderer resolves these links automatically:
- Default:
headroom://content/posts/01DEF→/{collection}/{slug}(i.e./posts/hello-world) - Custom resolver: Pass
resolveContentLinkto map to your site's URL structure - Broken links: Unpublished or missing references render as
#
You can also resolve links manually:
import type { PublicContentRef } from "@headroom-cms/api";
function resolveContentLink(ref: PublicContentRef): string {
if (!ref.published) return "#";
switch (ref.collection) {
case "posts": return `/blog/${ref.slug}`;
case "projects": return `/projects/${ref.slug}`;
default: return `/${ref.slug}`;
}
}Custom Block Components
React
Pass a components map to override built-in blocks or render custom block types:
import { BlockRenderer } from "@headroom-cms/api/react";
import type { BlockComponentProps } from "@headroom-cms/api/react";
function CallToAction({ block }: BlockComponentProps) {
return (
<div className="cta-banner">
<p>{block.props?.text as string}</p>
<a href={block.props?.url as string}>Learn more</a>
</div>
);
}
<BlockRenderer
blocks={blocks}
components={{ callToAction: CallToAction }}
/>Astro
For custom Astro blocks, create your own wrapper around the individual block components. Import and render the built-in components alongside your custom ones:
---
import Paragraph from "@headroom-cms/api/blocks/Paragraph.astro";
import Heading from "@headroom-cms/api/blocks/Heading.astro";
import Image from "@headroom-cms/api/blocks/Image.astro";
// ... other built-in imports
import MyCustomBlock from "../components/MyCustomBlock.astro";
const { blocks, refs, resolveContentLink } = Astro.props;
---
{blocks.map((block) => {
if (block.type === "myCustomBlock") return <MyCustomBlock block={block} />;
if (block.type === "paragraph") return <Paragraph block={block} refs={refs} resolveContentLink={resolveContentLink} />;
if (block.type === "heading") return <Heading block={block} refs={refs} resolveContentLink={resolveContentLink} />;
if (block.type === "image") return <Image block={block} />;
// ... handle remaining types
})}Relationships
Collections can define relationships to other collections. These are populated on single-content responses:
// Forward relationships (e.g. a project's "artists")
const project = await client.getContent("01ABC");
const artists = project.relationships?.artists; // ContentRef[]
// Reverse query: find all projects for an artist
const { items } = await client.listContent("projects", {
relatedTo: "01ARTIST",
relField: "artists",
});Media URLs
Media paths in content responses (block image URLs, cover images, field values) are stored as relative paths like /media/{site}/{mediaId}/original.jpg.
const client = new HeadroomClient({
url: "https://headroom.example.com",
site: "mysite.com",
apiKey: "headroom_xxxxx",
imageSigningSecret: "your-secret",
});
// Full URL for the original
client.mediaUrl(post.coverUrl);
// → "https://headroom.example.com/media/mysite.com/01ABC/original.jpg"
// Signed transform URL (resized, converted to webp)
client.transformUrl(post.coverUrl, { width: 800, format: "webp" });
// → "https://headroom.example.com/img/mysite.com/01ABC/original.jpg?format=webp&w=800&sig=abc123..."Transform Options
interface TransformOptions {
width?: number; // Target width in pixels
height?: number; // Target height in pixels
fit?: "cover" | "contain" | "fill" | "inside" | "outside";
format?: "webp" | "avif" | "jpeg" | "png"; // Output format
quality?: number; // 1-100
}Transforms require imageSigningSecret in the client config. Without it, transformUrl() falls back to mediaUrl().
Astro Integration
Content Loader
Use headroomLoader() to load Headroom content into Astro's content layer:
// src/content.config.ts
import { defineCollection } from "astro:content";
import { headroomLoader } from "@headroom-cms/api/astro";
export const collections = {
posts: defineCollection({
loader: headroomLoader({ collection: "posts" }),
}),
pages: defineCollection({
loader: headroomLoader({ collection: "pages", bodies: true }),
}),
};Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| collection | string | — | Headroom collection name |
| bodies | boolean | false | Fetch full content bodies (not just metadata) |
| config | HeadroomConfig? | from env | Override client config |
| schema | ZodType? | — | Zod schema for type-safe data access |
The loader reads config from environment variables by default:
HEADROOM_URL=https://headroom.example.com
HEADROOM_SITE=mysite.com
HEADROOM_API_KEY=headroom_xxxxx
HEADROOM_IMAGE_SIGNING_SECRET=your-secret # optionalDev Refresh
Add headroomDevRefresh() to your Astro config for automatic content reloading during development:
// astro.config.mjs
import { headroomDevRefresh } from "@headroom-cms/api/astro";
export default defineConfig({
integrations: [headroomDevRefresh()],
});Polls the Headroom API for version changes (default: every 5 seconds) and triggers a content sync when content is updated in the admin UI.
Zod Schema Codegen
Generate type-safe Zod schemas from your Headroom collection definitions:
import { HeadroomClient } from "@headroom-cms/api";
import { generateZodSchemas } from "@headroom-cms/api/codegen";
const client = new HeadroomClient({ /* ... */ });
const code = await generateZodSchemas(client);
// Write `code` to a file (e.g. src/lib/schemas.ts)This generates a TypeScript file with Zod schemas for each collection, ready to pass to headroomLoader({ schema }). See the sample site for a working example with a generate-schemas.sh script.
Styling
React
Import the default stylesheet:
import "@headroom-cms/api/react/headroom-blocks.css";Styles use low-specificity :where() selectors, making them easy to override. Customize via CSS custom properties:
| Property | Default | Used by |
|----------|---------|---------|
| --hr-code-bg | #f3f4f6 | Inline code background |
| --hr-link-color | #2563eb | Link color |
| --hr-image-radius | 0.5rem | Image border radius |
| --hr-caption-color | #6b7280 | Image caption color |
| --hr-code-block-bg | #1e1e1e | Code block background |
| --hr-code-block-color | #d4d4d4 | Code block text color |
| --hr-accent | #2563eb | Checkbox accent color |
| --hr-table-header-bg | #f9fafb | Table header background |
Astro
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.
TypeScript Types
All types are exported from the main entry point:
import type {
// Config
HeadroomConfig,
TransformOptions,
// Content
ContentItem,
ContentMetadata,
ContentListResult,
BatchContentResult,
// Blocks
Block,
InlineContent,
TextContent,
LinkContent,
TextStyles,
TableContent,
TableRow,
// References
ContentRef,
PublicContentRef,
RefsMap,
// Collections
Collection,
CollectionSummary,
CollectionListResult,
FieldDef,
RelationshipDef,
// Block Types
BlockTypeDef,
BlockTypeListResult,
} from "@headroom-cms/api";React-specific types:
import type { BlockRendererProps, BlockComponentProps, BlockComponentMap } from "@headroom-cms/api/react";Building & Publishing
pnpm build # Build all entry points (ESM + CJS + types)
pnpm test # Run tests
pnpm test:watch # Run tests in watch mode
pnpm typecheck # TypeScript type checking
pnpm dev # Watch mode buildPackage Entry Points
| Import path | Format | Description |
|-------------|--------|-------------|
| @headroom-cms/api | ESM + CJS | API client and types |
| @headroom-cms/api/react | ESM + CJS | React block renderer components |
| @headroom-cms/api/react/headroom-blocks.css | CSS | Default block styles |
| @headroom-cms/api/blocks/* | Astro source | Astro block components (compiled by consumer) |
| @headroom-cms/api/astro | ESM | Astro content loader + dev refresh |
| @headroom-cms/api/codegen | ESM + CJS | Zod schema generation |
npm Publish
pnpm build
npm publish --access publicThe files field in package.json includes only dist/ and blocks/ directories.
License
PolyForm Noncommercial 1.0.0
