What lives in Apostrophe
- Page types and piece types
- Widget schemas and field definitions
- The admin editing UI
- Content storage and the REST API that serves it
A short introduction to the architecture of ApostropheCMS headless project with Astro frontends tied together with our `apostrophe-astro` integration package.
How responsibility is split between your Apostrophe project and your Astro project, and why that split exists
What the @apostrophecms/apostrophe-astro bridge package actually does
How the template and widget registries connect Apostrophe content to Astro components — and the naming rules that trip people up
How area fields and the AposArea component let editors manage content without touching your templates
Where to go for setup steps, full configuration options, and deeper architectural reference
If you're coming to ApostropheCMS from its traditional Nunjucks or JSX templating, the Astro integration is a different way of building the same site: Apostrophe still owns content, but Astro owns rendering.
That's a meaningful shift, not just a swap of template languages. It means your team can use Astro's component model, its framework interoperability (React, Vue, Svelte, or plain Astro components), and its build tooling, while keeping the ApostropheCMS content modeling, schema system, and in-context editing UI exactly as they are.
When you adopt this integration, you end up with two codebases working together as one application:
The convention is to keep both in backend and frontend subdirectories of a single repository, which keeps versioning and deployment aligned.
This split has a practical payoff: a frontend developer can build and rework components without touching CMS configuration, and someone shaping content architecture can add or change fields without breaking a template. The two layers move independently as long as the contract between them — the registries described below — stays accurate.
Apostrophe owns content structure and editing.
The bridge package fetches and hands off content.
Astro owns every template and how content renders.
The @apostrophecms/apostrophe-astro package is what makes these two projects feel like one site rather than two services awkwardly stitched together. It's installed in your Astro project (not your Apostrophe project), and it provides four things:
A data-fetching helper. aposPageFetch runs in your single [...slug].astro route and authenticates with the Apostrophe backend, fetching the page document — or piece, for a piece-type show page — along with all of its widget data in one request. Authentication between the two projects is handled by a shared secret value, so the Apostrophe backend knows the request is legitimately coming from your Astro frontend rather than the public internet.
Layout and template components. AposLayout provides the page shell — head tags, body structure, and the switch between the normal site and the in-context editing experience for logged-in users. AposTemplate looks up and renders the correct Astro component for whatever page or piece was fetched, based on the template registry.
The AposArea component. This is what lets an editable area of content — built from any number of widgets — render correctly in Astro while preserving Apostrophe's add/remove/reorder editing controls. More on this below.
The in-context editing overlay. This is what makes it possible for an editor to click directly on a live Astro-rendered page and edit content, the same experience they'd have on a traditional Apostrophe site.
This is the part of the integration most worth understanding before you start building, because it's also the most common source of "why isn't my widget showing up" bugs.
There's no automatic, filename-based discovery connecting Apostrophe content to Astro components. Instead, two explicit registry files do the matching:
The matching rule is strict: the key in each registry has to match the type name exactly as ApostropheCMS stores it in the database — not necessarily the name of the backend module file.
For widgets, that means stripping -widget from the module name and including the @apostrophecms/ namespace prefix for core widgets:
| You might assume | What's actually needed |
|---|---|
'rich-text': RichTextWidget | '@apostrophecms/rich-text': RichTextWidget |
'button-widget': ButtonWidget | 'button': ButtonWidget |
For page and piece templates, a module that serves more than one template — like the ApostropheCMS blog, which has both a listing page and an individual post page — uses a colon suffix: '@apostrophecms/blog-page:index' and '@apostrophecms/blog-page:show'. If you create your own piece type later, follow the same 'your-piece-page:index' / 'your-piece-page:show' pattern.
A missing or mismatched template registry key (page type or piece type) appears to fail harder, since there's no fallback component to render in its place. See the architecture quick-reference for exact registry file locations and setup steps.
An area field is what gives editors the ability to add, remove, and reorder widgets on a page without any developer involvement. The split of responsibility here mirrors the project split described above:
AposArea and let it render. It doesn't need to know what's inside.This means a content architect can change which widgets are available in a given area, in the backend schema, without anyone touching the Astro side. AposArea also handles nesting — an area can live inside a widget's own fields, which is how layout-style, multi-column widgets work.
A few patterns show up consistently across the integration and are worth internalizing early:
_ (like _image or _author) is a relationship field that Apostrophe resolves at request time. These are always returned as arrays, even when the schema limits the relationship to one item — so post._author?.[0]?.title is the standard access pattern, not post._author.title. For images specifically, that same [0] step is as far as you go by hand: widget._image?.[0] gets you the image object, which you then hand to the helper functions above rather than reaching further into it yourself.The Astro integration splits responsibility cleanly: Apostrophe owns content and editing, Astro owns rendering — connected by the @apostrophecms/apostrophe-astro bridge package.
The bridge package provides a data-fetching helper, layout and template components, the AposArea component, and the in-context editing overlay.
Two explicit registry files — one for templates, one for widgets — connect Apostrophe content types to Astro components. Key names must match exactly, including the @apostrophecms/ prefix and stripped widget suffix.
Registry mismatches behave differently depending on type: a missing widget key logs an error and produces no markup, while a missing template key fails harder.
Area fields keep content structure in the backend schema; Astro templates just render what's handed to them via AposArea.
Always use the bridge package's image helper functions rather than navigating the attachment object manually.
Editors always work in SSR mode — that's what enables in-context editing. Static output is a publishing option, not a separate editorial workflow: the published site can be deployed as SSR or static, but content editing always happens in SSR.