Frontmatter

Astro PHP templates for WordPress — the clean approach

How to structure PHP templates generated from Astro so they work cleanly with WordPress — partials, escaping, variable passing, and what not to do.

A clean WordPress theme can start from an Astro project.

Not because Astro runs in WordPress. Because Astro is the right tool for building UI, and plain PHP partials are the right format for WordPress to consume.

The gap between the two is a mechanical translation. This post covers how to do it cleanly.

What “clean” means in this context

A clean PHP template for WordPress:

  • has one responsibility — rendering a data structure
  • receives variables as a plain PHP array
  • never fetches its own data
  • uses WordPress escaping functions consistently
  • matches the HTML structure of the Astro component exactly
<?php
// partials/card.php — generated by FM Solo
// Expects: $fm['props']['title'], $fm['props']['excerpt'], $fm['props']['url'], $fm['props']['image']
?>
<article class="card">
  <a href="<?= esc_url($fm['props']['url']) ?>">
    <img src="<?= esc_url($fm['props']['image']) ?>" alt="<?= esc_attr($fm['props']['title']) ?>" />
    <h2><?= esc_html($fm['props']['title']) ?></h2>
    <p><?= esc_html($fm['props']['excerpt']) ?></p>
  </a>
</article>

No WordPress API calls inside the partial. No get_the_title(). No the_post(). Just variables in, HTML out.

The variable passing convention

Every partial receives a single array named after the component:

// You write this — Solo generates the partial, not the data wiring
$fm = [
  'props' => [
    'title'   => get_the_title(),
    'excerpt' => get_the_excerpt(),
    'url'     => get_permalink(),
    'image'   => get_the_post_thumbnail_url(),
  ]
];

include get_template_directory() . '/partials/card.php';

The page template is responsible for fetching data from WordPress and passing it to the partial. The partial is responsible for rendering it. Never mix the two.

This pattern maps directly to Astro’s component model — the page fetches, the component renders.

Handling optional fields

In Astro, optional props are marked with ?. In PHP, they translate to null checks:

export interface Props {
  title: string;       // required
  excerpt?: string;    // optional
  badge?: string;      // optional
}
<?php // partials/card.php ?>
<article class="card">
  <?php if (!empty($card['badge'])): ?>
    <span class="badge"><?= esc_html($card['badge']) ?></span>
  <?php endif; ?>
  <h2><?= esc_html($fm['props']['title']) ?></h2>
  <?php if (!empty($fm['props']['excerpt'])): ?>
    <p><?= esc_html($fm['props']['excerpt']) ?></p>
  <?php endif; ?>
</article>

Use !empty() rather than isset() — it handles both missing keys and empty strings in one check.

Handling arrays — post loops

When an Astro component receives an array of items:

export interface Props {
  posts: { title: string; url: string; date: string }[];
}

The PHP equivalent uses foreach:

<?php // partials/post-list.php ?>
<ul class="post-list">
  <?php foreach ($fm['data']['posts'] as $post): ?>
    <li>
      <a href="<?= esc_url($post['url']) ?>">
        <?= esc_html($post['title']) ?>
      </a>
      <time><?= esc_html($post['date']) ?></time>
    </li>
  <?php endforeach; ?>
</ul>

What the INTEGRATION.md tells the WordPress developer

When Frontmatter Solo generates the render pack, the INTEGRATION.md contains an entry for every partial:

## Partial: card

| Variable       | Type    | Required | Notes                    |
|----------------|---------|----------|--------------------------|
| fm.props.title     | string  | yes      |                          |
| fm.props.excerpt   | string  | no       | truncate to 160 chars    |
| fm.props.url       | string  | yes      | full permalink           |
| fm.props.image     | string  | no       | post thumbnail URL       |
| fm.props.badge     | string  | no       | e.g. "New", "Sale"      |

The WordPress developer reads this once and knows exactly what to wire in each page template. No guessing. No reading the Astro source.

What Solo generates vs what you write

Solo generates:

  • all partials with correct variable slots and escaping
  • page templates with include calls
  • base layout (header/footer structure)
  • manifest.json with the full variable map
  • INTEGRATION.md with per-partial documentation

You write:

  • functions.php — enqueue scripts, register menus, define theme support
  • data fetching in page templates — get_field(), get_post_meta(), WP_Query
  • any WordPress-specific hooks or filters

The split is clean. Solo owns the rendering layer. WordPress owns the data layer.

Frontmatter Solo focuses on generating templates and structure. It does not replace WordPress theme development or backend logic.


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.