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 compose pages from reusable templates
  • Build your own components — cards, callouts, anything
  • 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>
    <?php App::render('/pages/' . $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::render('/pages/about').

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 anywhere:

App::render('/components/_card', [
    'title'   => 'Fast',
    'body'    => '117k requests per second',
    'variant' => 'highlight',
]);

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 built with nested App::render() calls. The "You will learn" box, this "Try it" block, the nav, the sidebar — each is a separate component template being rendered with App::render().

See the render method in action: Open render demo →

🔎 Preview: two more render methods

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

MethodReturnsUse when
render()void (echoes)Direct output — this lessonOpen →
renderToString()stringhtmx fragments — Lesson 8Open →
renderStream()GeneratorSSR streaming — Lesson 9Open →

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::render('/components/_card', ['title' => 'Hi']) do?

Key Takeaways

  • App::render('/_master', [...]) wraps pages in a shared layout
  • Components are PHP files that echo HTML with variables — no framework overhead
  • Variables are extracted from the render array into the template's scope
  • For now you only need render() — two more methods come in later lessons