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:
{# templates/product/index.twig — generated by FM Solo #}
{% extends 'layouts/base.twig' %}
{% block title %}{{ fm.page.title }}{% endblock %}
{% block content %}
{% include 'partials/hero.twig' with { fm: { props: fm.data.hero } } %}
{% for product in fm.data.products %}
{% include 'partials/product-card.twig' with { fm: { props: product } } %}
{% endfor %}
{% endblock %}
The base layout:
{# templates/layouts/base.twig — generated by FM Solo #}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% include 'partials/nav.twig' with { fm: { props: fm.data.nav } } %}
{% block content %}{% endblock %}
{% include 'partials/footer.twig' with { fm: { props: fm.data.footer } } %}
</body>
</html>
The Symfony controller
The controller’s only responsibility is passing the right variables to the template:
<?php
// src/Controller/ProductController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products', name: 'product_index')]
public function index(): Response
{
return $this->render('product/index.twig', [
'fm' => [
'page' => ['title' => 'Our products'],
'data' => [
'hero' => [
'headline' => 'Our products',
'sub' => 'Everything we make, in one place.',
],
'products' => $this->getProducts(),
],
],
]);
}
}
The controller passes variables under the fm namespace. The fm.page key carries page-level data, fm.data carries template-specific data, and partials receive their data as fm.props. 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
## Template: product/index.twig
Variables passed from controller:
| Variable | Type | Required | Description |
|-------------------|----------|----------|--------------------------|
| fm.page.title | string | yes | HTML title tag |
| fm.data.hero.headline | string | yes | Main heading |
| fm.data.hero.sub | string | no | Subheading |
| fm.data.products | array | yes | Array of product objects |
| fm.data.products[].name | string | yes | |
| fm.data.products[].price | float | yes | |
| fm.data.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.twig → templates/product/index.twig
├─ layouts/base.twig → templates/layouts/base.twig
└─ partials/hero.twig → templates/partials/hero.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 generates the full Symfony-compatible render pack from your Astro source:
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.
Frontmatter Solo does not generate Symfony controllers or application logic. It generates Twig templates and their expected data contract only.
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.