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 <Hero headline="..." /> becomes {{ fm.props.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:
{# partials/hero.twig #}
<section class="hero">
<h1>{{ fm.props.headline }}</h1>
{% if hero.sub %}
<p>{{ fm.props.sub }}</p>
{% endif %}
{% if hero.ctaLabel %}
<a href="{{ fm.props.ctaHref }}">{{ fm.props.ctaLabel }}</a>
{% endif %}
</section>
The structure is identical to your Astro component. The only difference is syntax.
The Astro component you’re starting from
---
export interface Props {
headline: string;
sub?: string;
ctaLabel?: string;
ctaHref?: string;
}
const { headline, sub, ctaLabel, ctaHref } = Astro.props;
---
<section class="hero">
<h1>{headline}</h1>
{sub && <p>{sub}</p>}
{ctaLabel && <a href={ctaHref}>{ctaLabel}</a>}
</section>
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 → {{ fm.props.headline }}
sub?: string → {% if hero.sub %}{{ fm.props.sub }}{% endif %}
Optional props (?) become {% if %} blocks. Required props are output directly.
Slot → block
<slot />
becomes:
{% block content %}{% endblock %}
Conditional rendering
{condition && <element />}
becomes:
{% if condition %}<element />{% endif %}
Array iteration
{items.map(item => <Card {...item} />)}
becomes:
{% for item in fm.data.items %}
{% include 'partials/card.twig' with { fm: { props: item } } %}
{% endfor %}
The page file
An Astro page becomes a Twig page that extends the base layout:
{# pages/index.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
{% include 'partials/hero.twig' with { fm: { props: fm.data.hero } } %}
{% include 'partials/section.twig' with { fm: { props: fm.data.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.twig
├─ layouts/
│ └─ base.twig
├─ partials/
│ ├─ hero.twig
│ ├─ section.twig
│ └─ footer.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 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.
frontmatter solo:build --adapter twig
The output follows the same structure as a manual conversion. 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 is usually cleaner.
Frontmatter Solo generates templates and a variable contract. Integration with your backend remains explicit and project-specific.
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.
Scope
Frontmatter Solo:
- generates templates (Twig or PHP)
- defines a variable contract (manifest + INTEGRATION.md)
It does not:
- integrate with your backend
- fetch data
- configure a CMS
- provide runtime behavior
Integration is explicit and owned by the host application.