WordPress powers 40% of the web. Astro is where modern frontends are built.
Getting them to work together is not complicated — but it requires a clear mental model of what maps to what.
The mental model
An Astro component is a PHP partial waiting to happen.
---
export interface Props {
headline: string;
sub?: string;
}
const { headline, sub } = Astro.props;
---
<section class="hero">
<h1>{headline}</h1>
{sub && <p>{sub}</p>}
</section>
Becomes:
<?php // partials/hero.php — generated by FM Solo ?>
<section class="hero">
<h1><?= esc_html($fm['props']['headline']) ?></h1>
<?php if (!empty($fm['props']['sub'])): ?>
<p><?= esc_html($fm['props']['sub']) ?></p>
<?php endif; ?>
</section>
Same structure. Same class names. Same conditional logic. Different syntax.
The WordPress theme folder structure
A standard WordPress theme maps cleanly to the Solo output:
wp-content/themes/your-theme/
├─ index.php ← from output/pages/
├─ page.php ← from output/pages/
├─ header.php ← from output/layouts/
├─ footer.php ← from output/layouts/
├─ partials/
│ ├─ hero.php ← from output/partials/
│ └─ section.php ← from output/partials/
└─ functions.php ← you write this
Astro layouts map to WordPress header/footer templates. The Astro pages become page templates. The Astro components become partials.
Wiring variables in WordPress
The generated INTEGRATION.md lists every variable each partial expects. For a hero partial with headline and sub, the WordPress developer wires them like this:
With ACF (Advanced Custom Fields):
<?php // page-home.php
get_header();
$fm = [
'props' => [
'headline' => get_field('hero_headline'),
'sub' => get_field('hero_sub'),
]
];
include get_template_directory() . '/partials/hero.php';
get_footer();
With post meta:
$fm = [
'props' => [
'headline' => get_post_meta(get_the_ID(), 'hero_headline', true),
'sub' => get_post_meta(get_the_ID(), 'hero_sub', true),
]
];
With hardcoded content (for static sections):
$fm = [
'props' => [
'headline' => 'Welcome to our site',
'sub' => null,
]
];
The partial doesn’t care where the data comes from. It receives an array and renders it.
What to use instead of esc_html
Solo generates htmlspecialchars() by default — standard PHP escaping. If you’re integrating into WordPress, you can replace it with esc_html() and esc_url(). The templates are plain PHP files, fully editable.
The full conversion workflow
- Build the site in Astro with typed
Propsinterfaces on every component - Run
frontmatter solo:build --adapter php - Copy
output/partials/andoutput/layouts/into the WordPress theme - Use
INTEGRATION.mdto wire variables in each page template - Write
functions.php— this is the only file Solo doesn’t generate
Frontmatter Solo handles steps 1 through 4 automatically. Step 5 is intentionally left to the WordPress developer — it’s the one file that is genuinely project-specific.
Frontmatter Solo does not generate a complete WordPress theme. It generates templates and structure only.
Theme configuration (functions.php, hooks, CMS wiring) remains the responsibility of the backend developer.
Why not use a headless WordPress setup
Headless WordPress (REST API or WPGraphQL) requires maintaining two separate deployments — the WordPress backend and the Astro frontend. That’s fine for large teams, but it adds operational complexity that most WordPress projects don’t need.
The Solo approach is simpler: the Astro project is the design source, Solo generates the WordPress theme, and everything runs on a single WordPress installation. No second server. No API coupling. No CORS headaches.
The tradeoff is that you can’t use Astro’s routing or islands in production — the WordPress theme is purely server-rendered PHP. For most WordPress sites, that’s exactly what you want.
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.