Converting a static HTML/CSS design to Twig is mostly translation work.
The HTML is already there. The class names are already there. What changes is the data model: hard-coded content becomes variables, optional regions become conditions, repeating sections become loops.
The problem with converting HTML to Twig by hand
A static HTML design has values everywhere in plain text: titles, image paths, button labels, and repeated content blocks. Converting that by hand means deciding which values are dynamic, inventing variable names, and wiring conditionals one by one.
On a 10-component design, that quickly turns into several hours of work. The output is usually inconsistent because every developer names things a little differently.
What the mapping looks like
The conversion rules are mechanical:
- Static value ->
{{ fm.props.headline }} - Optional block ->
{% if fm.props.sub %}...{% endif %} - Array of items ->
{% for item in fm.data.products %}...{% endfor %} - Layout ->
{% extends 'layouts/base.twig' %}+{% block content %}
Example:
<section class="hero">
<h1>Welcome to our site</h1>
<p>We build things that last.</p>
<a href="/contact/">Get in touch</a>
</section>
Becomes:
{# partials/hero.twig #}
<section class="hero">
<h1>{{ fm.props.headline }}</h1>
{% if fm.props.sub %}
<p>{{ fm.props.sub }}</p>
{% endif %}
{% if fm.props.ctaLabel %}
<a href="{{ fm.props.ctaHref }}">{{ fm.props.ctaLabel }}</a>
{% endif %}
</section>
The structure stays the same. Only the hard-coded values disappear.
The full output structure
A complete render pack should include:
partials/- one.twigfile per componentlayouts/- a base layout with blockspages/- one page template per routemanifest.json- every variable expected by each templateINTEGRATION.md- what the backend must pass in
That structure is what makes the handoff usable. The backend developer gets files they can drop into templates/ and a contract they can trust.
Why using Astro as an intermediate step helps
Astro gives you typed Props interfaces and a predictable component structure before you convert anything. That means the conversion is not guessed from raw HTML. It is derived from a validated source of truth.
Required props map to required Twig variables. Optional props map to {% if %} blocks. Slots map to {% block %} regions. The mapping is deterministic, which is why it is automatable.
solo-check is the fastest way to verify that your Astro project follows the constrained structure Solo needs before you generate output.
Automating it with Frontmatter Solo
Frontmatter Solo turns the mapping into a single build step:
frontmatter solo:build --adapter twig
That command generates the render pack, the manifest, and the integration guide. Use Frontmatter Solo when the frontend design is already structured in Astro and you want the Twig output without manual translation.
Framework compatibility
The generated Twig output is framework-agnostic and works with:
- Symfony - drop the files into
templates/ - Drupal - adapt the include paths to the theme layer
- Craft CMS - same Twig syntax, same structure
- Statamic - standard Twig templates
- Standalone Twig - any PHP project using Twig