# Frontmatter — llms-full.txt
# Full content index for AI models and LLM crawlers
# Generated: 2026-04-01
# Site: https://www.frontmatter.tech
========================================================================
## ABOUT FRONTMATTER
Frontmatter is a suite of build-time tools for Astro developers built by Alexandre Desane, a frontend developer based in Paris, France.
Alexandre has years of experience building editorial frontends at scale — Le Monde, Télérama — on PHP stacks with thousands of daily readers. Frontmatter comes from the recurring problem of handing off Astro projects to PHP and Symfony backends: the frontend was done, the backend developer was waiting, and someone had to manually translate every component into templates, every prop into a variable.
Frontmatter automates that translation. The Astro Props interface is already the variable contract — Solo extracts it at build time and generates the full render pack.
Contact: hello@frontmatter.tech
Author site: https://alxdsn.fr
GitHub: https://github.com/alexandredesane
========================================================================
## PRODUCTS
### Frontmatter Core
URL: https://www.frontmatter.tech/core
License: MIT (open source)
GitHub: https://github.com/withfrontmatter/frontmatter-core
A build-time content layer engine for Astro. Scans .astro, .md, and .yaml files and outputs a stable typed JSON Intermediate Representation (IR). The IR is the foundation every other Frontmatter tool builds on. CMS adapters and paid tools plug into the same IR.
Key concept: The frontend defines structure. The CMS adapts — not the other way around.
Input: .astro files, .md files, .yaml files
Output: JSON IR (pages with routes and typed fields, datasets as explicit collections)
Example IR output:
{ "pages": [{ "route": "/", "fields": ["title", "subtitle"] }], "datasets": ["works", "posts"] }
### Frontmatter Solo
URL: https://www.frontmatter.tech/solo
Price: $49 one-time — no subscription
Delivery: CLI (macOS, Linux, Windows via Node.js) + Desktop app (macOS, built with Tauri)
Reads an Astro project at build time and generates a complete server-side render pack. No rewrite. No runtime dependency. No coupling between frontend and backend.
Output formats:
--adapter twig → Twig templates (Symfony, Drupal, Craft CMS, any Twig backend)
--adapter php → Plain PHP includes (WordPress, custom CMS, vanilla PHP)
What gets generated:
output/
├─ pages/ # One file per Astro page
├─ layouts/ # Base layout templates
├─ partials/ # One partial per Astro component
├─ manifest.json # Machine-readable variable map
└─ INTEGRATION.md # Per-template variable documentation
The mapping rules:
Required prop (string) → {{ var }} / $arr['var']
Optional prop (string?) → {% if var %} / if (!empty($var))
Array prop → {% for item in items %} / foreach
Slot → {% block content %} / include point
Stack-specific pages:
WordPress: https://www.frontmatter.tech/solo/wordpress
Symfony: https://www.frontmatter.tech/solo/symfony
Twig: https://www.frontmatter.tech/solo/twig
### Frontmatter Studio
URL: https://www.frontmatter.tech/studio
Status: Coming soon
From Astro to a fully editable site with Grav CMS wired in. No backend developer required. Clients get a clean editing interface, developers get a one-command deploy.
========================================================================
## BLOG ARTICLES
Total articles: 12
------------------------------------------------
Title: Why CMS break modern frontends (Astro makes it obvious)
URL: https://www.frontmatter.tech/blog/astro-exposes-cms-problem/
Date: 2026-01-12
Description: CMS are great at content, but they model structure too late. Astro exposes the mismatch — build-time IR fixes it.
Tags: ["astro", "cms", "content-modeling", "build-time", "ir"]
Modern frontends are fast.
CMS are powerful.
But they don’t speak the same language.
Astro components speak in props.
CMS speak in data models.
The real problem is not content management.
The real problem is where content modeling happens.
The classic workflow destroys frontend quality
Most CMS workflows force developers to rebuild structure inside a backoffice:
- layouts get reimplemented as “templates”
- components become “blocks”
- designers hand off, and quality drops
- editors can accidentally break structure
You end up maintaining two systems that drift apart.
Astro exposes a better contract
In Astro, a component already defines an implicit contract:
- exported props define what is configurable
- pages define routes and composition
- datasets exist as structured inputs
The structure is already there — in code.
Build-time is the only place this can be clean
Runtime integrations are expensive:
- extra dependencies
- runtime performance costs
- more failure modes
- lock-in to a specific CMS API
A better approach is build-time only:
- scan the project
- extract the structure
- output a stable data contract (JSON)
A build-time translation step
The problem is not rendering.
The problem is where translation happens.
Frontmatter introduces a build-time translation step between your frontend code and any CMS.
What the IR actually looks like
The output of this build-time step is not content.
It’s structure, expressed as data.
Pages become routes with typed fields.
Datasets become explicit collections.
This is the kind of output Frontmatter produces:
This JSON is a contract.
It can be consumed by:
- a flat-file CMS
- a headless CMS
- a custom backend
- or just checked into git
Once structure becomes data, CMS become interchangeable.
What this is not
This approach is not for everyone.
It is not:
- a CMS
- a site builder
- a visual editor
- a runtime framework
- a solution for highly dynamic, user-generated content
It assumes:
- a code-first workflow
- a static or build-time oriented frontend
- a clear separation between structure and content
If your CMS needs to control layout, this is the wrong tool.
A data contract makes CMS interchangeable
Once structure becomes data, the CMS becomes a tool to edit content — not a place to rebuild your design.
Developers keep layout in code.
Editors touch structured content.
Modern frontends don’t need “better CMS features”.
They need a clearer contract.
------------------------------------------------
Title: Layout is code, content is data
URL: https://www.frontmatter.tech/blog/layout-is-code-content-is-data/
Date: 2026-01-13
Description: A simple rule that prevents CMS-driven rewrites: keep layout in code, expose content as structured data.
Tags: ["architecture", "astro", "cms", "workflow"]
If you only remember one thing, remember this:
Layout stays in code.
Content becomes data.
Why this rule matters
CMS are built to manage content: text, media, metadata, lists.
They are not built to preserve frontend intent: spacing, composition, interaction boundaries, component contracts.
When a CMS starts owning layout, you get:
- page builders
- fragile templates
- design drift
- endless “can we make this block do X?” tickets
The boring workflow that scales
A scalable workflow is boring:
1. Build the site in Astro first.
2. Define what is editable via component props and content frontmatter.
3. Extract a content model at build-time.
4. Let editors change data, not structure.
This keeps teams aligned:
- developers own structure
- editors own content
- no one fights the system
The outcome
You ship faster, with fewer regressions.
Not because the CMS is better.
Because the responsibilities are finally in the right place.
------------------------------------------------
Title: Why we don’t ship CMS adapters in the core
URL: https://www.frontmatter.tech/blog/why-we-dont-ship-cms-adapters-in-the-core/
Date: 2026-01-14
Description: CMS adapters create hidden coupling and support debt. Keeping them outside the core is a deliberate architectural choice.
Tags: ["architecture", "cms", "ir", "open-core", "astro"]
One of the most common questions we get is simple:
“Which CMS does Frontmatter support?”
The short answer is:
none — in the core.
This is not a limitation.
It’s a deliberate architectural decision.
The real cost of shipping adapters
CMS adapters look harmless.
They promise:
- faster onboarding
- easier demos
- broader appeal
In practice, they introduce hidden costs.
Every adapter:
- couples your core to someone else’s roadmap
- imports a foreign data model
- creates compatibility expectations
- becomes a permanent support surface
Once an adapter lives in the core, the question is no longer “does this work?”
It becomes “why doesn’t this work with my setup?”
That’s not a technical problem.
It’s a product trap.
The core needs a single responsibility
Frontmatter Core does one thing:
It turns frontend code into a stable data contract.
That contract is the product.
The moment the core starts:
- talking to CMS APIs
- generating CMS-specific config
- adapting to editor workflows
…it stops being a contract and starts becoming an integration layer.
Integration layers never stop growing.
Adapters invert the ownership model
Most CMS-first workflows assume this:
> The CMS defines structure.
> The frontend adapts.
Frontmatter inverts that:
> The frontend defines structure.
> The CMS adapts.
If the core ships adapters, ownership quietly shifts back to the CMS.
Suddenly:
- the CMS becomes the source of truth
- the frontend is constrained again
- the IR becomes an internal detail instead of a contract
That defeats the entire purpose.
A stable IR is more valuable than any adapter
Adapters age.
CMS APIs change.
Editor features evolve.
Integrations break.
A stable Intermediate Representation does not.
By keeping the core CMS-agnostic:
- the IR stays boring
- adapters stay optional
- experimentation stays cheap
- failure stays local
You can:
- write your own adapter
- fork an existing one
- throw it away entirely
The core doesn’t care.
Reference implementations are not commitments
Frontmatter uses CMS integrations as reference implementations, not guarantees.
They exist to show:
- how the IR can be consumed
- what a mapping might look like
- how far you can take the workflow
They do not define the product.
The core does not promise:
- compatibility
- support
- completeness
- coverage across CMS ecosystems
That boundary is explicit.
This keeps the project honest
Not shipping adapters in the core forces discipline.
It prevents:
- roadmap drift
- integration sprawl
- accidental product promises
It also attracts the right users:
- developers who value explicit contracts
- teams comfortable owning their tooling
- people who prefer boring infrastructure over magic
If you’re looking for a tool that “just plugs into everything”, this isn’t it.
The trade-off is intentional
Yes, this approach is stricter.
It assumes:
- a code-first workflow
- comfort with build-time tooling
- a willingness to wire things together explicitly
In exchange, you get:
- no runtime dependency
- no CMS lock-in
- no hidden coupling
- no forced abstractions
The core stays small.
The surface stays sharp.
The rule we don’t break
Frontmatter does not adapt to CMS.
CMS adapt to the IR.
That rule keeps the project coherent.
Breaking it would make adoption easier in the short term — and impossible to sustain in the long term.
We choose the long term.
------------------------------------------------
Title: How to convert an Astro project to Twig templates
URL: https://www.frontmatter.tech/blog/astro-to-twig-templates/
Date: 2026-01-15
Description: A step-by-step breakdown of what it takes to turn Astro components into Twig partials — manually, and with a build-time tool.
Tags: ["astro", "twig", "backend-handoff", "solo", "symfony", "drupal"]
Most Astro projects end the same way.
The frontend is done. It looks good. The backend developer is waiting.
And then someone has to sit down and explain how becomes {{ hero.headline }} in a Twig template.
This post breaks down exactly how that conversion works — the logic behind it, the manual approach, and why most teams automate it.
What Twig templates expect
Twig is the templating engine behind Symfony, Drupal, and several other PHP frameworks. It uses a syntax that maps cleanly to variables:
``twig
{# partials/hero.html.twig #}
{{ hero.headline }}
{% if hero.sub %}
{{ hero.sub }}
{% endif %}
{% if hero.ctaLabel %}
{{ hero.ctaLabel }}
{% endif %}
`
The structure is identical to your Astro component. The only difference is syntax.
The Astro component you're starting from
`astro
---
export interface Props {
headline: string;
sub?: string;
ctaLabel?: string;
ctaHref?: string;
}
const { headline, sub, ctaLabel, ctaHref } = Astro.props;
---
{headline}
{sub &&
{sub}
}
{ctaLabel && {ctaLabel}}
`
The Props interface is your contract. Every prop becomes a Twig variable. The HTML structure stays exactly the same.
The mapping rules
Converting Astro to Twig follows a small set of consistent rules:
Props → variables
`
headline: string → {{ hero.headline }}
sub?: string → {% if hero.sub %}{{ hero.sub }}{% endif %}
`
Optional props (?) become {% if %} blocks. Required props are output directly.
Slot → block
`astro
`
becomes:
`twig
{% block content %}{% endblock %}
`
Conditional rendering
`astro
{condition && }
`
becomes:
`twig
{% if condition %}{% endif %}
`
Array iteration
`astro
{items.map(item => )}
`
becomes:
`twig
{% for item in items %}
{% include 'partials/card.html.twig' with item %}
{% endfor %}
`
The page file
An Astro page becomes a Twig page that extends the base layout:
`twig
{# pages/index.html.twig #}
{% extends 'layouts/base.html.twig' %}
{% block content %}
{% include 'partials/hero.html.twig' with hero %}
{% include 'partials/section.html.twig' with section %}
{% endblock %}
`
The layout becomes a base template with named blocks. The page composes partials exactly like the Astro page composed components.
What you need to deliver
A complete handoff package for a Twig backend includes:
`
output/
├─ pages/
│ └─ index.html.twig
├─ layouts/
│ └─ base.html.twig
├─ partials/
│ ├─ hero.html.twig
│ ├─ section.html.twig
│ └─ footer.html.twig
├─ manifest.json
└─ INTEGRATION.md
`
The manifest.json maps each template to its expected variables. The INTEGRATION.md tells the backend developer what to plug in where.
The manual problem
Done once, this is manageable.
Done across 8 components, 4 layouts, and 3 pages — with an interface Props that changes every sprint — it becomes a maintenance problem.
Every time you update a prop in Astro, you have to update the Twig partial, the manifest, and the integration doc. By hand.
This is exactly what Frontmatter Solo (/solo) automates. It reads your Astro project at build time, extracts the Props interfaces, maps them to Twig variables, and generates the full render pack — pages, partials, manifest, and INTEGRATION.md — in a single command.
`bash
frontmatter solo:build --adapter twig
`
The output is identical to what you'd write manually. Except it stays in sync automatically.
When to use Twig output
Twig makes sense when:
- the backend runs Symfony or Drupal
- the team is already comfortable with Twig syntax
- you want a templating layer that feels native to PHP frameworks
If the backend is WordPress, a custom CMS, or anything PHP-based without a framework, plain PHP output (/blog/astro-php-template-generator/) is usually cleaner.
The underlying principle
Your Astro component already contains the full spec for its Twig equivalent.
The Props interface is the variable contract. The HTML structure is the template body. The conditional rendering maps directly to {% if %}` blocks.
Nothing has to be reinvented. It just has to be translated — consistently, completely, and once.
------------------------------------------------
Title: Why “AI-built websites” don’t age well
URL: https://www.frontmatter.tech/blog/why-ai-built-websites-dont-age-well/
Date: 2026-01-22
Description: AI can generate websites in minutes. But when trends, tools, and business models change, content-driven architectures are the only ones that survive.
Tags: ["architecture", "frontend", "ai", "content-modeling", "build-time"]
“AI builds the site for you.”
At first, that sounds like the end of frontend development.
Why design, structure, or think ahead if a model can generate everything in minutes?
But ask a simple question:
What happens in three years?
Trends move faster than content
Frameworks change.
Design trends rotate.
Tools pivot, get acquired, change pricing, or disappear.
We’ve seen this cycle many times already:
- Flash
- jQuery-heavy CMS themes
- Visual page builders
- JavaScript frameworks that promised “write once, rewrite never”
The pattern is always the same.
The presentation layer moves fast.
The content does not.
Your pages, articles, case studies, documentation, and marketing copy are expected to live for years. Sometimes decades.
Any system that treats content as a side effect of a tool is already on a timer.
Generated websites restart from zero
Most AI website builders generate:
- React components
- runtime-driven layouts
- implicit data models
- tightly coupled structure and content
It looks impressive on day one.
But when you want to:
- redesign
- change framework
- migrate away
- escape a vendor
- adapt to a new product direction
You don’t evolve the site.
You restart it.
Because:
- the structure is implicit
- the content is trapped inside the tool
- the model only exists as code generated for a specific runtime
Redesign becomes migration.
Migration becomes rewrite.
Content-driven systems age differently
A content-driven system starts with a boring assumption:
> Content must outlive tools.
That leads to boring choices:
- Markdown
- YAML
- explicit data structures
- files in a repository
- build-time extraction
Nothing fancy. Nothing trendy.
But it creates a powerful property:
Your content is portable.
It is readable.
It is versionable.
It is independent from how it is rendered today.
Frameworks can change.
Design can change.
Even entire frontend stacks can change.
The content stays.
Rebuilding the skin is not rebuilding the site
When content is structured and explicit:
- redesign is just re-integration
- not a rewrite
- not a migration
- not a rescue operation
You replace:
- CSS
- components
- layout
- interaction patterns
You do not recreate:
- pages
- models
- editorial structure
- years of content decisions
> You don’t rebuild a site.
> You re-skin it.
This is the difference between a website as a product
and a website as an output.
Why boring wins over time
“Boring” formats survive because they don’t try to be clever.
Markdown doesn’t need a startup to exist.
YAML doesn’t depend on a roadmap.
Static files don’t change business models.
Boring systems:
- are explicit
- degrade gracefully
- are understandable by future developers
- don’t require a platform to stay alive
Trends are runtime concerns.
Content is a long-term asset.
Treating them the same is the original mistake.
AI changes velocity, not responsibility
AI absolutely changes how fast we build interfaces.
It does not remove the need to decide:
- what is content
- what is structure
- what is editable
- what must remain stable
- what can change safely
Those decisions define whether a site ages well or becomes technical debt.
No model takes that responsibility for you.
Final thought
AI can generate a website.
Architecture decides whether it still exists in ten years.
If your system forces you to start over every time the trend changes,
the problem was never the tool.
It was the absence of a content contract.
------------------------------------------------
Title: Backend handoff from Astro — the clean way
URL: https://www.frontmatter.tech/blog/backend-handoff-astro/
Date: 2026-01-29
Description: What a proper Astro-to-backend handoff looks like, what usually goes wrong, and how to structure it so the backend developer can start immediately.
Tags: ["astro", "backend-handoff", "solo", "workflow", "twig", "php"]
The Astro project is done.
Every component is built, every page is composed, every prop is typed. The frontend developer hands it over to the backend team.
And then nothing happens for two weeks.
Not because the backend developer is slow. Because they're spending that time reverse-engineering what the frontend means.
What usually goes wrong
A typical handoff package is:
- a zip of the Astro source
- a Loom video walking through the components
- a Notion page with "here are the main components and what they do"
None of that is wrong. But none of it answers the only question that matters for a backend developer:
For each page, which variables do I need to provide, in what shape, at what route?
The Notion doc describes intent. The backend developer needs a contract.
What a clean handoff looks like
A proper Astro-to-backend handoff delivers three things:
1. Templates that match the frontend structure
Not a description of the components. The actual templates — in Twig or plain PHP — with the correct HTML structure, correct class names, and correct variable slots.
``twig
{# partials/hero.html.twig #}
{{ hero.headline }}
{% if hero.sub %}
{{ hero.sub }}
{% endif %}
`
The backend developer doesn't have to read Astro. They get files they can use directly.
2. A variable manifest
A machine-readable map of what variables each template expects:
`json
{
"partials": {
"hero": {
"headline": { "type": "string", "required": true },
"sub": { "type": "string", "required": false }
}
}
}
`
This is the contract. The backend developer knows exactly what to inject without reading the frontend source.
3. An integration guide
A human-readable document that explains, per page:
- which templates are composed
- which variables are required
- where the data is expected to come from (CMS field, database column, API response)
`markdown
Page: /
Templates: layouts/base.html.twig → pages/index.html.twig
Partials:
- hero: { headline, sub?, ctaLabel?, ctaHref? }
- section: { title, text? }
Data source: connect to your CMS or database per field above.
`
With these three artifacts, the backend developer can start on day one.
The structural insight
An Astro component already contains everything needed to generate this package.
The Props interface is the variable manifest. The JSX structure is the template body. The page composition is the routing and include structure.
`astro
---
export interface Props {
headline: string; // → {{ hero.headline }}
sub?: string; // → {% if hero.sub %}
}
---
{headline}
{sub &&
{sub}
}
`
Nothing has to be reinvented. The structure is already there in the Astro source. It just has to be extracted and translated.
Why this is usually done manually — and shouldn't be
Most teams do this translation by hand, once, at the end of the project.
The problems start when the frontend changes:
- a new prop is added to a component
- an optional prop becomes required
- a component is split into two
- a page is restructured
Every change requires updating the templates, the manifest, and the integration guide. By hand. With no guarantee of consistency.
The correct approach is to generate the handoff package from the Astro source automatically — at build time, every time.
Frontmatter Solo (/solo) does exactly this. It reads your Astro project, extracts the Props interfaces, maps them to Twig or PHP templates, and outputs the full package in a single command:
`bash
frontmatter solo:build --adapter twig
or
frontmatter solo:build --adapter php
`
The output is:
`
output/
├─ pages/
├─ layouts/
├─ partials/
├─ manifest.json
└─ INTEGRATION.md
`
Same structure. Same variables. Always in sync with the Astro source.
The rule
A clean handoff is not documentation.
It's a contract — expressed as templates, manifest, and integration guide — that the backend developer can use without reading the frontend source.
If your backend developer has to open an .astro` file to understand what variables to provide, the handoff is not done.
Generate it. Don't write it.
------------------------------------------------
Title: Generating PHP templates from an Astro project
URL: https://www.frontmatter.tech/blog/astro-php-template-generator/
Date: 2026-02-09
Description: How to convert Astro components to plain PHP include-based templates — without a framework, without a runtime, and without a rewrite.
Tags: ["astro", "php", "backend-handoff", "solo", "wordpress", "templates"]
Twig is clean. But not every backend runs Symfony or Drupal.
WordPress, custom CMS, legacy PHP codebases, agency projects built on vanilla PHP — all of them need templates. None of them need Twig.
For those backends, the output format is plain PHP includes.
What plain PHP templates look like
No framework. No templating engine. Just PHP includes and variables:
``php
= htmlspecialchars($hero['headline']) ?>
= htmlspecialchars($hero['sub']) ?>
= htmlspecialchars($hero['ctaLabel']) ?>
`
The structure is identical to the Astro component. The HTML is the same. The class names are the same. The variables are the same — expressed as $hero['headline'] instead of {{ hero.headline }}.
The Astro component you're starting from
`astro
---
export interface Props {
headline: string;
sub?: string;
ctaLabel?: string;
ctaHref?: string;
}
const { headline, sub, ctaLabel, ctaHref } = Astro.props;
---
{headline}
{sub &&
{sub}
}
{ctaLabel && {ctaLabel}}
`
The conversion rules are mechanical. Every prop maps to a PHP array key. Every optional prop becomes a null check.
The mapping rules
Props → PHP variables
`
headline: string → $hero['headline']
sub?: string → !empty($hero['sub']) ? $hero['sub'] : ''
`
Page includes
A page file includes its partials and passes data as arrays:
`php
$page['hero']['headline'],
'sub' => $page['hero']['sub'] ?? null,
];
include __DIR__ . '/../partials/hero.php';
?>
`
Layout
The base layout wraps the page with the shared structure:
`php
= htmlspecialchars($page['title'] ?? '') ?>
`
Array iteration
`astro
{items.map(item => )}
`
becomes:
`php
`
Why plain PHP over Twig
Twig requires a templating engine. Plain PHP doesn't.
For backends that don't already run Symfony or Drupal, adding Twig means adding a dependency. Plain PHP includes work anywhere PHP runs — WordPress, custom CMS, legacy codebases, shared hosting.
The tradeoff is verbosity. {% if hero.sub %} is cleaner than . But if your backend developer doesn't want another dependency, the verbose option is the right one.
The full output structure
A plain PHP render pack mirrors the Twig output exactly:
`
output/
├─ pages/
│ └─ index.php
├─ layouts/
│ └─ base.php
├─ partials/
│ ├─ hero.php
│ ├─ section.php
│ └─ footer.php
├─ manifest.json
└─ INTEGRATION.md
`
The manifest.json is format-agnostic — same structure whether you're outputting Twig or PHP. The INTEGRATION.md lists every variable expected by every template.
Generating it automatically
The conversion from Astro to PHP follows the same mechanical rules every time. Which means it can be automated.
Frontmatter Solo (/solo) reads your Astro project at build time and generates the full PHP render pack from your component Props interfaces:
`bash
frontmatter solo:build --adapter php
`
The output is ready to drop into any PHP backend. The backend developer gets templates that match the frontend exactly, with every variable documented.
Switching between Twig and PHP is a single flag change:
`bash
frontmatter solo:build --adapter twig # for Symfony, Drupal
frontmatter solo:build --adapter php # for WordPress, custom PHP
`
Same Astro source. Same intermediate representation. Different syntax.
The principle
Whether you're targeting Twig or plain PHP, the approach is the same:
Your Astro Props` interface is the variable contract. Your HTML structure is the template body. Your component composition is the include structure.
Nothing has to be reinvented. The conversion is mechanical. The only question is whether you do it by hand or generate it.
------------------------------------------------
Title: Why frontend developers should own the backend handoff
URL: https://www.frontmatter.tech/blog/frontend-devs-own-the-handoff/
Date: 2026-02-19
Description: The handoff is not a backend problem. It's a frontend artifact — and the frontend developer is the only one positioned to generate it correctly.
Tags: ["backend-handoff", "workflow", "astro", "solo", "frontend"]
The traditional workflow puts handoff at the end.
Frontend ships. Backend receives. Someone in between writes documentation. Everyone hopes it matches.
It rarely does.
Where the mismatch comes from
The backend developer receives Astro source files they didn't write, in a framework they may not use daily, with a component structure that made perfect sense to the frontend developer and is opaque to everyone else.
The questions are always the same:
- What variables does this page need?
- Which fields are required vs optional?
- Where does this data come from?
- What's the expected shape of this array?
These are not backend questions. They are frontend questions, because the answers are encoded in the frontend source — in the Props interfaces, in the component composition, in the page structure.
The backend developer is asking the frontend developer's code to explain itself. In a language neither of them speaks at that moment.
The frontend source already contains the handoff
An Astro component with a typed Props interface is already a specification:
``astro
---
export interface Props {
headline: string; // required
sub?: string; // optional
ctaLabel?: string; // optional
ctaHref?: string; // optional, pairs with ctaLabel
}
---
`
This interface tells you:
- exactly which variables the template needs
- which are required and which are not
- the type of each variable
Every question the backend developer will ask is answerable from this file.
The problem is that the backend developer shouldn't have to read it. They should receive something already translated.
Who is positioned to generate the translation
The frontend developer.
Not because they're responsible for the backend. Because they're the only one who can read the source correctly at the moment it matters.
The backend developer receives the Astro project after it's done. They didn't watch it being built. They don't know which props were added last week, which components were split, which optional fields became required.
The frontend developer knows all of this — because they built it.
The correct workflow is:
1. Frontend developer builds in Astro
2. Frontend developer generates the handoff package from their own source
3. Backend developer receives a complete, accurate package on day one
Step 2 is the one that usually doesn't happen.
What "owning the handoff" looks like in practice
It means the frontend developer is responsible for producing:
- Twig or PHP templates that match the Astro structure
- a variable manifest listing every prop by template
- an integration guide explaining what data goes where
Not as documentation written at the end. As a build artifact generated automatically.
`bash
frontmatter solo:build --adapter twig
`
This command reads the Astro source, extracts the Props interfaces, and outputs:
`
output/
├─ pages/
├─ layouts/
├─ partials/
├─ manifest.json
└─ INTEGRATION.md
`
The frontend developer runs one command. The backend developer gets everything they need.
Why this changes the timeline
The standard handoff takes two weeks to actually start — not because anyone is slow, but because the back-and-forth of "what does this mean?" is slow.
When the handoff package is generated from the source, the backend developer can start immediately. There's nothing to interpret. There's nothing to ask.
The frontend developer doesn't spend time answering questions they've already answered in their code. The backend developer doesn't spend time reverse-engineering a framework they don't use.
Both sides work from the same contract. Generated, not negotiated.
The shift in responsibility
This is not about putting more work on the frontend developer.
It's about recognizing that the work already exists — encoded in the Props` interfaces, the component structure, the page composition — and making it visible as a deliverable instead of leaving it implicit.
The frontend source is the spec. The handoff package is just the spec, translated.
Frontmatter Solo (/solo) does the translation. The frontend developer stays the author.
------------------------------------------------
Title: How to turn an Astro project into a WordPress theme
URL: https://www.frontmatter.tech/blog/astro-to-wordpress-theme/
Date: 2026-02-27
Description: A practical guide to converting Astro components into WordPress-compatible PHP templates — partials, page templates, and variable wiring — without a full rewrite.
Tags: ["astro", "wordpress", "php", "backend-handoff", "solo"]
WordPress powers 40% of the web. Astro is where modern frontends are built.
Getting them to work together is not complicated — but it requires a clear mental model of what maps to what.
The mental model
An Astro component is a PHP partial waiting to happen.
``astro
---
export interface Props {
headline: string;
sub?: string;
}
const { headline, sub } = Astro.props;
---
{headline}
{sub &&
{sub}
}
`
Becomes:
`php
= esc_html($hero['headline']) ?>
= esc_html($hero['sub']) ?>
`
Same structure. Same class names. Same conditional logic. Different syntax.
The WordPress theme folder structure
A standard WordPress theme maps cleanly to the Solo output:
`
wp-content/themes/your-theme/
├─ index.php ← from output/pages/
├─ page.php ← from output/pages/
├─ header.php ← from output/layouts/
├─ footer.php ← from output/layouts/
├─ partials/
│ ├─ hero.php ← from output/partials/
│ └─ section.php ← from output/partials/
└─ functions.php ← you write this
`
The Astro layouts become WordPress header/footer. The Astro pages become page templates. The Astro components become partials.
Wiring variables in WordPress
The generated INTEGRATION.md lists every variable each partial expects. For a hero partial with headline and sub, the WordPress developer wires them like this:
With ACF (Advanced Custom Fields):
`php
get_field('hero_headline'),
'sub' => get_field('hero_sub'),
];
include get_template_directory() . '/partials/hero.php';
get_footer();
`
With post meta:
`php
$hero = [
'headline' => get_post_meta(get_the_ID(), 'hero_headline', true),
'sub' => get_post_meta(get_the_ID(), 'hero_sub', true),
];
`
With hardcoded content (for static sections):
`php
$hero = [
'headline' => 'Welcome to our site',
'sub' => null,
];
`
The partial doesn't care where the data comes from. It receives an array and renders it.
What to use instead of esc_html
Solo generates esc_html() and esc_url() by default — standard WordPress escaping functions. If your backend developer prefers a different approach, the templates are plain PHP files, fully editable.
The full conversion workflow
1. Build the site in Astro with typed Props interfaces on every component
2. Run frontmatter solo:build --adapter php
3. Copy output/partials/ and output/layouts/ into the WordPress theme
4. Use INTEGRATION.md to wire variables in each page template
5. Write functions.php` — this is the only file Solo doesn't generate
Frontmatter Solo (/solo/wordpress) handles steps 1 through 4 automatically. Step 5 is intentionally left to the WordPress developer — it's the one file that is genuinely project-specific.
Why not use a headless WordPress setup
Headless WordPress (REST API or WPGraphQL) requires maintaining two separate deployments — the WordPress backend and the Astro frontend. That's fine for large teams, but it adds operational complexity that most WordPress projects don't need.
The Solo approach is simpler: the Astro project is the design source, Solo generates the WordPress theme, and everything runs on a single WordPress installation. No second server. No API coupling. No CORS headaches.
The tradeoff is that you can't use Astro's routing or islands in production — the WordPress theme is purely server-rendered PHP. For most WordPress sites, that's exactly what you want.
------------------------------------------------
Title: Astro PHP templates for WordPress — the clean approach
URL: https://www.frontmatter.tech/blog/astro-php-wordpress-templates/
Date: 2026-03-07
Description: How to structure PHP templates generated from Astro so they work cleanly with WordPress — partials, escaping, variable passing, and what not to do.
Tags: ["astro", "wordpress", "php", "backend-handoff", "solo", "templates"]
The cleanest WordPress theme you can deliver to a client starts in Astro.
Not because Astro runs in WordPress. Because Astro is the right tool for building UI, and plain PHP partials are the right format for WordPress to consume.
The gap between the two is a mechanical translation. This post covers how to do it cleanly.
What "clean" means in this context
A clean PHP template for WordPress:
- has one responsibility — rendering a data structure
- receives variables as a plain PHP array
- never fetches its own data
- uses WordPress escaping functions consistently
- matches the HTML structure of the Astro component exactly
``php
`
No WordPress API calls inside the partial. No get_the_title(). No the_post(). Just variables in, HTML out.
The variable passing convention
Every partial receives a single array named after the component:
`php
// Calling a partial
$card = [
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'url' => get_permalink(),
'image' => get_the_post_thumbnail_url(),
];
include get_template_directory() . '/partials/card.php';
`
The page template is responsible for fetching data from WordPress and passing it to the partial. The partial is responsible for rendering it. Never mix the two.
This pattern maps directly to Astro's component model — the page fetches, the component renders.
Handling optional fields
In Astro, optional props are marked with ?. In PHP, they translate to null checks:
`astro
export interface Props {
title: string; // required
excerpt?: string; // optional
badge?: string; // optional
}
`
`php
= esc_html($card['badge']) ?>
= esc_html($card['title']) ?>
= esc_html($card['excerpt']) ?>
`
Use !empty() rather than isset() — it handles both missing keys and empty strings in one check.
Handling arrays — post loops
When an Astro component receives an array of items:
`astro
export interface Props {
posts: { title: string; url: string; date: string }[];
}
`
The PHP equivalent uses foreach:
`php
`
What the INTEGRATION.md tells the WordPress developer
When Frontmatter Solo (/solo/wordpress) generates the render pack, the INTEGRATION.md contains an entry for every partial:
`markdown
Partial: card
| Variable | Type | Required | Notes |
|----------------|---------|----------|--------------------------|
| card.title | string | yes | |
| card.excerpt | string | no | truncate to 160 chars |
| card.url | string | yes | full permalink |
| card.image | string | no | post thumbnail URL |
| card.badge | string | no | e.g. "New", "Sale" |
`
The WordPress developer reads this once and knows exactly what to wire in each page template. No guessing. No reading the Astro source.
What Solo generates vs what you write
Solo generates:
- all partials with correct variable slots and escaping
- page templates with include calls
- base layout (header/footer structure)
- manifest.json with the full variable map
- INTEGRATION.md with per-partial documentation
You write:
- functions.php — enqueue scripts, register menus, define theme support
- data fetching in page templates — get_field(), get_post_meta()`, WP_Query
- any WordPress-specific hooks or filters
The split is clean. Solo owns the rendering layer. WordPress owns the data layer.
------------------------------------------------
Title: Integrating an Astro frontend with a Symfony backend
URL: https://www.frontmatter.tech/blog/astro-twig-symfony-guide/
Date: 2026-03-16
Description: How to hand off an Astro project to a Symfony developer — Twig templates, controller variables, and the integration pattern that keeps both sides clean.
Tags: ["astro", "twig", "symfony", "backend-handoff", "solo"]
Symfony and Astro are a natural pair — Astro for building UI, Symfony for application logic, Twig as the shared rendering layer between them.
The challenge is the handoff. This post covers the integration pattern that keeps both sides clean.
The integration model
Astro → Solo → Twig templates → Symfony controller → rendered page.
The Astro project is the design source. Solo translates it to Twig. The Symfony controller provides the data. Twig renders it.
None of these steps are coupled. The Astro developer doesn't need to know Symfony. The Symfony developer doesn't need to read Astro.
What the Twig output looks like
A Symfony-compatible Twig template follows the standard extends / block pattern:
``twig
{# templates/product/index.html.twig — generated by FM Solo #}
{% extends 'layouts/base.html.twig' %}
{% block title %}{{ page.title }}{% endblock %}
{% block content %}
{% include 'partials/hero.html.twig' with { hero: hero } %}
{% for product in products %}
{% include 'partials/product-card.html.twig' with { product: product } %}
{% endfor %}
{% endblock %}
`
The base layout:
`twig
{# templates/layouts/base.html.twig — generated by FM Solo #}
{% block title %}{% endblock %}
{% include 'partials/nav.html.twig' with { nav: nav } %}
{% block content %}{% endblock %}
{% include 'partials/footer.html.twig' with { footer: footer } %}
`
The Symfony controller
The controller's only responsibility is passing the right variables to the template:
`php
render('product/index.html.twig', [
'page' => [
'title' => 'Our products',
],
'hero' => [
'headline' => 'Our products',
'sub' => 'Everything we make, in one place.',
],
'products' => $this->getProducts(), // your data source
]);
}
}
`
The variable names in the controller match the variable names in the Twig templates exactly. The INTEGRATION.md generated by Solo documents what each template expects — the Symfony developer reads it once, writes the controller, done.
What INTEGRATION.md tells the Symfony developer
`markdown
Template: product/index.html.twig
Variables passed from controller:
| Variable | Type | Required | Description |
|-------------------|----------|----------|--------------------------|
| page.title | string | yes | HTML title tag |
| hero.headline | string | yes | Main heading |
| hero.sub | string | no | Subheading |
| products | array | yes | Array of product objects |
| products[].name | string | yes | |
| products[].price | float | yes | |
| products[].image | string | no | Image URL |
`
No ambiguity. No back-and-forth.
Dropping the templates into Symfony
The generated output maps directly to Symfony's templates/ directory:
`
output/
├─ pages/index.html.twig → templates/product/index.html.twig
├─ layouts/base.html.twig → templates/layouts/base.html.twig
└─ partials/hero.html.twig → templates/partials/hero.html.twig
`
Copy the files. Update the extends paths if your template namespace differs. Wire the controller variables per INTEGRATION.md. Done.
Generating the Twig output
Frontmatter Solo (/solo/symfony) generates the full Symfony-compatible render pack from your Astro source:
`bash
frontmatter solo:build --adapter twig
`
Output:
`
output/
├─ pages/
├─ layouts/
├─ partials/
├─ manifest.json
└─ INTEGRATION.md
`
The manifest.json is machine-readable — you can use it to validate controller variables automatically or generate Symfony form types if needed.
The clean boundary
The Astro developer and the Symfony developer never need to talk about template details. Solo handles the translation. The INTEGRATION.md` handles the communication.
Both sides work from the same contract — generated from the Astro source, not negotiated in a Notion doc.
------------------------------------------------
Title: Astro project delivery checklist — what to ship to your backend developer
URL: https://www.frontmatter.tech/blog/astro-project-delivery-checklist/
Date: 2026-03-27
Description: A practical checklist for frontend developers delivering an Astro project to a backend team. What to include, what to generate, and what to never hand off manually.
Tags: ["astro", "backend-handoff", "workflow", "checklist", "solo", "twig", "php"]
You're done with the Astro project.
Before you hand it off, go through this list.
---
1. Every component has a typed Props interface
``astro
---
export interface Props {
headline: string;
sub?: string;
}
---
`
If a component doesn't have a typed Props interface, the backend developer has no contract to work from. Add it before you ship.
Why it matters: The Props interface is what build-time tools like Frontmatter Solo (/solo) use to generate the variable manifest. No interface = no automation.
---
2. Optional props are correctly marked
`astro
export interface Props {
headline: string; // required — backend must always provide this
sub?: string; // optional — backend can omit this
}
`
Every ? is a signal to the backend developer that they don't have to provide that value. Every missing ? is a guarantee they do.
Get this right before shipping. Fixing it after the templates are generated means regenerating everything.
---
3. No hardcoded content in components
If a component renders a hardcoded string instead of a prop, it won't be editable by the backend:
`astro
Welcome to our site
{headline}
`
Scan your components for hardcoded content before generating the handoff.
---
4. Slots are used where content should be composable
If a layout or section should accept arbitrary content from the page:
`astro
{title}
`
Slots translate to Twig blocks or PHP include-points. If a slot is missing where it should exist, the backend developer won't have a way to inject content.
---
5. Generate the render pack — don't write it
At this point, run:
`bash
frontmatter solo:build --adapter twig
or
frontmatter solo:build --adapter php
`
You should get:
`
output/
├─ pages/
├─ layouts/
├─ partials/
├─ manifest.json
└─ INTEGRATION.md
`
If you don't have Frontmatter Solo, you can get it here (/solo) for $49 one-time. It pays for itself on the first project where you've had to answer "what does this prop do?" more than twice.
---
6. Review the INTEGRATION.md before sending
Open INTEGRATION.md and read it as if you're the backend developer seeing this project for the first time.
Check that:
- every page has its list of required variables
- every partial has its expected data shape documented
- optional fields are clearly marked
- there are no variables listed as required that the backend can't reasonably provide
This document is the backend developer's starting point. If it's unclear to you, it will be unclear to them.
---
7. Include a preview if the adapter supports it
Frontmatter Solo can generate a preview-php/ directory — a plain PHP rendering of the templates with stub data. This lets the backend developer verify the output structure before wiring real data.
If your backend runs Twig, a preview-twig/ equivalent works the same way.
---
8. Don't ship the Astro source as the handoff
The Astro source is not the deliverable. It's the origin.
Shipping .astro files to a backend developer who works in Symfony, Drupal, or WordPress is the equivalent of shipping a Figma file to someone who needs a PDF.
They can open it. It doesn't help them.
The deliverable is the render pack: templates, manifest, integration guide. Generated from the source. Not the source itself.
---
9. Agree on the adapter format upfront
Ask the backend developer before you generate anything:
- Twig: Symfony, Drupal, Craft CMS, any Twig-capable framework
- PHP: WordPress, custom CMS, legacy PHP, anything without a templating engine
Running the wrong adapter wastes time for both sides.
---
10. Tag the version
Before handing off, tag the Astro source in git:
`bash
git tag handoff-v1
`
This gives both sides a stable reference point. If props change after handoff, the backend developer can diff against the tag to see exactly what changed in the contract.
---
The short version
1. Type your props
2. Mark optionals correctly
3. Remove hardcoded content
4. Add slots where needed
5. Run frontmatter solo:build
6. Review INTEGRATION.md`
7. Ship the output folder, not the source
The backend developer should be able to start immediately. If they have questions, the handoff is not done.
========================================================================
## TOPIC CLUSTERS
### Astro backend handoff
URL: https://www.frontmatter.tech/blog/astro-backend-handoff/
Description: How to hand off an Astro project to a backend developer. Covers Twig and PHP output, variable manifests, and integration documentation. Supported by 5 articles.
Articles:
- https://www.frontmatter.tech/blog/astro-to-twig-templates/
- https://www.frontmatter.tech/blog/backend-handoff-astro/
- https://www.frontmatter.tech/blog/astro-php-template-generator/
- https://www.frontmatter.tech/blog/frontend-devs-own-the-handoff/
- https://www.frontmatter.tech/blog/astro-project-delivery-checklist/
### Astro to WordPress handoff
URL: https://www.frontmatter.tech/blog/astro-wordpress-handoff/
Description: Converting Astro components to WordPress-compatible PHP templates. PHP partials, variable wiring with ACF or post meta, theme structure.
Articles:
- https://www.frontmatter.tech/blog/astro-to-wordpress-theme/
- https://www.frontmatter.tech/blog/astro-php-wordpress-templates/
- https://www.frontmatter.tech/blog/astro-php-template-generator/
- https://www.frontmatter.tech/blog/astro-project-delivery-checklist/
### Astro to Symfony handoff
URL: https://www.frontmatter.tech/blog/astro-symfony-handoff/
Description: Integrating an Astro frontend with a Symfony backend. Twig templates, controller variables, and the INTEGRATION.md contract.
Articles:
- https://www.frontmatter.tech/blog/astro-twig-symfony-guide/
- https://www.frontmatter.tech/blog/astro-to-twig-templates/
- https://www.frontmatter.tech/blog/backend-handoff-astro/
### Astro to Twig
URL: https://www.frontmatter.tech/blog/astro-twig-handoff/
Description: The complete Astro-to-Twig mapping guide. Works with Symfony, Drupal, Craft CMS, Statamic, and any Twig-capable backend.
Articles:
- https://www.frontmatter.tech/blog/astro-to-twig-templates/
- https://www.frontmatter.tech/blog/astro-twig-symfony-guide/
- https://www.frontmatter.tech/blog/backend-handoff-astro/
### CMS & content modeling
URL: https://www.frontmatter.tech/blog/cms-content-modeling/
Description: Why CMS and modern frontends don't speak the same language, and how build-time extraction fixes it. Covers IR, content contracts, and the layout-in-code principle.
Articles:
- https://www.frontmatter.tech/blog/layout-is-code-content-is-data/
- https://www.frontmatter.tech/blog/astro-exposes-cms-problem/
- https://www.frontmatter.tech/blog/why-we-dont-ship-cms-adapters-in-the-core/
- https://www.frontmatter.tech/blog/why-ai-built-websites-dont-age-well/
========================================================================
# End of llms-full.txt
# Character count: ~55,084