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, hydrationHow variables flow
When you call App::render('/_master', ['title' => 'About', 'page' => 'about']):
- ZealPHP loads
template/_master.php - The array values are extracted as local variables:
$title = 'About',$page = 'about' - The template runs with those variables in scope
- 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.
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:
| Method | Returns | Use when | |
|---|---|---|---|
render() | the result (echoed too) | A page or a response — this lesson | Open → |
component() | markup, never echoed | Composing pieces; htmx fragments — Lesson 18 | Open → |
render(..., stream: true) | Generator | SSR streaming — Lesson 20 | Open → |
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