Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

Layouts & Components

Stop copy-pasting HTML. Extract shared structure into reusable templates.

You will learn

  • Why copy-pasting layouts across pages is a maintenance nightmare
  • Use App::render() to render a page inside a shared layout
  • Build your own components — cards, callouts, anything — and compose them with App::component()
  • How template variables flow from handler to view

The problem

You have three pages. Each one needs the same <head>, navigation bar, and footer. Right now, you're copy-pasting that boilerplate into every file. When you change the nav, you change it in three places. When you have 20 pages, you change it in 20 places.

That's not sustainable. You need a layout.

Step 1: Create a master layout

A layout is just a PHP template that wraps your page content. Create template/_master.php:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title><?= htmlspecialchars($title ?? 'My App') ?></title>
  <link rel="stylesheet" href="/css/site.css">
</head>
<body>
  <nav>...your nav here...</nav>
  <main>
    <?= App::component('/' . $page . '/' . basename($page)) ?>
  </main>
  <footer>...</footer>
</body>
</html>

Step 2: Use it from your page

Now your public/about.php becomes three lines:

<?php use ZealPHP\App;
App::render('/_master', ['title' => 'About', 'page' => 'about']);

App::render() loads template/_master.php, extracts the variables ($title and $page) into scope, and executes it. The master template then renders the page content with a nested App::component('/about/about') (the page's own folder, template/about/about.php), which returns the page's markup for <?= ?> to print. App::render() is for the page itself; App::component() is for markup you put inside it. A leading / means the path is absolute from template/.

Change the nav once in _master.php — every page picks it up. That's the power of a layout.

Building reusable components

The same pattern works for any reusable HTML. Think of a component as a stencil with holes — you lay the stencil down and fill in the holes with different data each time.

<!-- template/components/_card.php -->
<?php $variant ??= 'default'; ?>
<article class="card card-<?= htmlspecialchars($variant) ?>">
  <h3><?= htmlspecialchars($title) ?></h3>
  <p><?= $body ?></p>
</article>

Use it in any template:

<?= App::component('card', [
    'title'   => 'Fast',
    'body'    => '117k requests per second',
    'variant' => 'highlight',
]) ?>

App::component('card') looks nearest first: the page's folder (template/pricing/ for public/pricing.php), its components/, template/, then template/components/ — card.php before _card.php in each. So a card only the pricing page uses lives in template/pricing/components/card.php; this one, used everywhere, lives in template/components/. Same call either way. It returns the markup and never echoes it, so you print it with <?= ?>, concatenate it, or return it from a route. No class, no JSX, no bundler. A component is just a PHP file that echoes HTML with variables.

function Card({ title, body, variant = "default" }) {
  return (
    <article className={`card card-${variant}`}>
      <h3>{title}</h3>
      <p>{body}</p>
    </article>
  );
}

// Requires: React, JSX transpiler, bundler, hydration

How variables flow

When you call App::render('/_master', ['title' => 'About', 'page' => 'about']):

  1. ZealPHP loads template/_master.php
  2. The array values are extracted as local variables: $title = 'About', $page = 'about'
  3. The template runs with those variables in scope
  4. Output is captured and sent to the browser

Default values use PHP's null coalescing: $variant ??= 'default' sets a fallback when the caller doesn't pass variant.

▶ Live demo: render in action

This page you're reading is a layout rendered with App::render(). The "You will learn" box, this "Try it" block, the nav, the sidebar — each is a separate component template rendered inside it.

See both methods in action: Open render demo → Open component demo →

🔎 Preview: the rest of the rendering API

You'll use these in later lessons, but you can preview them now:

MethodReturnsUse when
render()the result (echoed too)A page or a response — this lessonOpen →
component()markup, never echoedComposing pieces; htmx fragments — Lesson 18Open →
render(..., stream: true)GeneratorSSR streaming — Lesson 20Open →

The streaming demo is the most visual — watch 12 rows arrive one by one over ~2 seconds. Each yield flushes a chunk to the browser immediately.

What does App::component('card', ['title' => 'Hi']) do?

Key Takeaways

  • App::render('/_master', [...]) wraps pages in a shared layout
  • <?= App::component('card', [...]) ?> composes pieces — it returns markup and never echoes
  • Components are PHP files that echo HTML with variables — no framework overhead
  • Variables are extracted from the render array into the template's scope