Frontmatter

How to convert HTML to Twig templates

A step-by-step guide to converting a static HTML/CSS design into Twig templates - variable mapping, conditional blocks, layout inheritance, and how to automate the whole process.

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 .twig file per component
  • layouts/ - a base layout with blocks
  • pages/ - one page template per route
  • manifest.json - every variable expected by each template
  • INTEGRATION.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