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 // partials/hero.php ?>
<section class="hero">
<h1><?= htmlspecialchars($fm['props']['headline']) ?></h1>
<?php if (!empty($fm['props']['sub'])): ?>
<p><?= htmlspecialchars($fm['props']['sub']) ?></p>
<?php endif; ?>
<?php if (!empty($fm['props']['ctaLabel'])): ?>
<a href="<?= htmlspecialchars($fm['props']['ctaHref']) ?>">
<?= htmlspecialchars($fm['props']['ctaLabel']) ?>
</a>
<?php endif; ?>
</section>
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 $fm['props']['headline'] instead of {{ fm.props.headline }}.
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 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 → $fm['props']['headline']
sub?: string → !empty($fm['props']['sub']) ? $fm['props']['sub'] : ''
Page includes
A page file includes its partials and passes data as arrays:
<?php // pages/index.php ?>
<?php include __DIR__ . '/../layouts/base.php'; ?>
<?php
// You populate $fm from your data source
$fm = [
'props' => [
'headline' => $page['hero']['headline'], // from your data source
'sub' => $page['hero']['sub'] ?? null,
]
];
include __DIR__ . '/../partials/hero.php';
?>
Layout
The base layout wraps the page with the shared structure:
<?php // layouts/base.php ?>
<!doctype html>
<html lang="en">
<head>
<title><?= htmlspecialchars($fm['page']['title'] ?? '') ?></title>
</head>
<body>
<?php // content is included by the page ?>
</body>
</html>
Array iteration
{items.map(item => <Card {...item} />)}
becomes:
<?php foreach ($fm['data']['items'] as $item): ?>
<?php // pass $item as fm.props to the partial
$fm['props'] = $item;
include __DIR__ . '/../partials/card.php';
<?php endforeach; ?>
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 <?php if (!empty($hero['sub'])): ?>. 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 reads your Astro project at build time and generates the full PHP render pack from your component Props interfaces:
frontmatter solo:build --adapter php
The output matches your frontend structure and can be integrated 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:
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.
Frontmatter Solo generates the rendering layer (templates and structure). It does not handle backend integration, data fetching, or CMS configuration.
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.
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.