@airdraft/react-content
v0.1.14
Published
Airdraft React components for rendering CMS content — field atoms, entry cards, and blog recipes
Readme
@airdraft/react-content
React components for rendering Airdraft CMS content. Provides MIME-aware field atoms, schema-driven entry cards and detail views, blog recipe wrappers, and Next.js next/image-backed media components.
Installation
npm install @airdraft/react-contentField Atoms
Low-level server components for rendering individual field values.
import { ImageField, MediaField, BodyField, DateField, TagList, AuthorByline, UrlField } from '@airdraft/react-content'| Component | Description |
|---|---|
| <ImageField> | Renders an image field key using <img>. Reads {field}_url or {field}_media.url from entry data. |
| <MediaField> | MIME-aware renderer for a single media field — <img>, <video>, <audio>, or <a> depending on type. Reads {field}_media. |
| <MediaListField> | Renders a media field with multiple: true as a list. Reads {field}_medias / {field}_urls. |
| <BodyField> | Renders rich-text / text fields as sanitised HTML. |
| <DateField> | Formats a date or datetime field value. |
| <TagList> | Renders list / multiselect fields as a tag row. |
| <AuthorByline> | Renders a resolved relation field as an author byline. |
| <UrlField> | Renders a url field as an <a> link. |
Schema-Driven Compounds
Generic components that render any entry from its collection schema without hard-coding field names.
Auto-layout mode
Pass entry and collection — the components discover field names from the schema automatically.
import { EntryCard, EntryDetail } from '@airdraft/react-content'
import { asCollectionConfig } from '@airdraft/core'
import schema from '@/airdraft.schema.json'
const postsCollection = asCollectionConfig(schema.collections.posts)
// Card: cover → title → excerpt → tags → date
<EntryCard entry={post} collection={postsCollection} />
// Detail: cover → header (title + date + url) → body → tags
<EntryDetail entry={post} collection={postsCollection} />Children injection mode
Pass children to take full control of layout. entry and collection are automatically injected into each sub-component — you don't need to pass them manually.
<EntryCard entry={post} collection={postsCollection}>
<EntryCard.Cover className="aspect-video object-cover" />
<EntryCard.Title className="text-xl font-bold" />
<EntryCard.Excerpt className="text-sm text-muted" />
<EntryCard.Tags />
<EntryCard.Date />
</EntryCard>
<EntryDetail entry={post} collection={postsCollection}>
<EntryDetail.Cover />
<EntryDetail.Header>
<EntryDetail.Tags />
{/* custom JSX mixed in freely */}
<h1 className="text-4xl">{post.data.title as string}</h1>
<EntryDetail.Link label="View project →" />
</EntryDetail.Header>
<EntryDetail.Body className="prose" />
</EntryDetail>EntryCard sub-components
| Sub-component | Auto-discovers | Explicit props |
|---|---|---|
| EntryCard.Cover | First image/media field | field, multiple |
| EntryCard.Title | First of title/name/heading/label | children |
| EntryCard.Excerpt | First text/rich-text field | children |
| EntryCard.Tags | First multiselect/list field | tags |
| EntryCard.Date | First date/datetime field | value |
| EntryCard.Link | First url field | value, label |
| EntryCard.Meta | — (layout wrapper) | children |
EntryDetail sub-components
| Sub-component | Auto-discovers | Explicit props |
|---|---|---|
| EntryDetail.Cover | First image/media field | field, multiple |
| EntryDetail.Header | — (layout wrapper) | children |
| EntryDetail.Body | First rich-text/text field | value, type |
| EntryDetail.Tags | First multiselect/list field | tags |
| EntryDetail.Link | First url field | value, label |
| EntryDetail.Meta | — (layout wrapper) | children |
Blog Recipes
Pre-built wrappers tailored for blog content:
import { PostCard, PostDetail } from '@airdraft/react-content'
<PostCard post={post} />
<PostDetail post={post} />These expect entries with conventional field names (title, excerpt, body, cover, publishedAt, tags, authors).
Next.js Sub-path
Use @airdraft/react-content/next for next/image-backed variants:
import { AirdraftImage, AirdraftMedia } from '@airdraft/react-content/next'
// next/image — uses stored width/height from media sidecar
<AirdraftImage entry={post} field="cover" />
// MIME-aware: next/image for images, <video>/<audio>/<a> for other types
<AirdraftMedia entry={post} field="hero" />Both components accept a render prop for fully custom rendering while keeping the resolution logic.
Content Utilities
The pure functions that back the field components are available separately in @airdraft/content — no React required. Use them in generateMetadata, server actions, API routes, or middleware.
import {
resolveMediaUrl, // single media/image field → URL string
resolveMediaUrls, // media field with multiple:true → string[]
resolveImageDimensions, // → { width, height } from media sidecar
parseMarkdown, // Markdown → sanitised HTML string
parseText, // plain text → string[] (paragraphs)
resolveRelation, // relation field → display label string
resolveRelations, // relations field → string[]
} from '@airdraft/content'
// Example: OG image metadata in Next.js
export async function generateMetadata({ params }) {
const post = await cms.getEntry('posts', params.slug)
const url = resolveMediaUrl('cover', post.data)
const dims = resolveImageDimensions('cover', post.data)
return { openGraph: { images: [{ url, ...dims }] } }
}Changelog
See CHANGELOG.md.
