Contentful Rendering
Use rendering adapters when Contentful owns nested, polymorphic page composition. Adapters keep CMS fields at the integration boundary while providing visual components and semantic Markdown from the same entry.
Create the adapter factory
Create one browser-safe factory from the generated type map:
import { createContentfulAdapterFactory } from "brix/contentful/rendering";
import type { Context } from "brix/hono";
import type { ContentTypeMap } from "#/contentful-types.ts";
export const createAdapter = createContentfulAdapterFactory<ContentTypeMap, Context>();
Only conventional *.adapter.tsx modules inspect Contentful fields and sys. Each exports exactly one PascalCase runtime value ending in Adapter, defined by a direct factory call with a literal contentType. Components receive ordinary application props.
import { md } from "brix/markdown";
import { createAdapter } from "#/contentful-rendering.ts";
import { Hero } from "./Hero.tsx";
export const HeroAdapter = createAdapter({
contentType: "hero",
markdown: ({ entry }) => md.heading(1, md.text(entry.fields.heading)),
component: ({ entry, preview }) => (
<Hero heading={entry.fields.heading} headingAttributes={preview?.getProps("heading")} />
),
});
An adapter may also define extra typed props, a bounded server load, and standalone preview fallbacks. Keep top-level imports browser-safe and dynamically import server-only code inside load. Pass the supplied AbortSignal to downstream requests. Loader results used during island hydration are browser-visible, so never return credentials or private data.
Render polymorphic entries
Vite generates the registry used by renderEntry:
import { renderEntry } from "brix/contentful/rendering/registry";
export function ContentPage({ entry }: { entry: UnknownEntry }) {
return <PageLayout>{renderEntry(entry)}</PageLayout>;
}
Entries must be returned in explicit page load props so Brix can prepare adapter loaders before synchronous SSR. Nested adapters can call the renderEntry supplied to their component. Pass an optional transport props record as the second argument for polymorphic children; directly render a known adapter when those props should be statically checked.
Add semantic Markdown
An adapter's synchronous markdown callback returns safe nodes from brix/markdown, not a string, promise, or complete document. It can recursively project child entries with its supplied renderEntry. Use markdown: false only for intentionally visual-only nested content; omitting required projections fails explicitly.
Represent the root in a page route and add the single document wrapper there:
import { markdownEntry } from "brix/contentful/rendering/registry";
import { md } from "brix/markdown";
export const articlePageRoute = createPageRoute(articlePageContract, {
component: ArticlePage,
load: async (c, { params }) => ({ entry: await getArticle(c, params.slug) }),
markdown: async (c, { entry }) => md.document({ body: await markdownEntry(entry, c) }),
});
Preparation and projection have bounded depth, node counts, concurrency, passes, and time. Loader failures remain typed loaded states so both visual and semantic output can provide truthful fallback content.
Project Contentful Rich Text
Keep visual Rich Text rendering in your application component. For agent-readable output, project the structured document directly rather than converting rendered HTML:
import { projectContentfulRichText } from "brix/contentful/rendering/markdown";
export const ArticleAdapter = createAdapter({
contentType: "article",
markdown: ({ entry, renderEntry }) =>
projectContentfulRichText(entry.fields.body, { renderEntry }),
component: ({ entry }) => <Article body={entry.fields.body} />,
});
The projector supports paragraphs, headings, lists, blockquotes, separators, GFM tables, text marks, safe hyperlinks, assets, and embedded entries. Embedded entries use the adapter registry. Supply getEntryUrl for entry hyperlinks; without it, readable labels remain but Brix invents no destination. Unsafe links and unsupported or malformed structures fail safely and explicitly.