Rendering Content
Component Patterns
How Bolt components work: author a reusable partial in the includes/components folder, give it a data array, include it in a page, and render its markup as raw HTML.
Overview
A component in Bolt is a small PHP partial that lives in the includes/components/ folder, accepts a single data array, and emits ready-to-render HTML. Instead of copy-pasting the same markup across many pages, you author the markup once as a component, then feed it different data wherever you need it.
Components are plain PHP — there are no classes, no template engine, and no build step. A component reads a variable you set before including it, prints markup based on that data, and that markup is captured into the page's output buffer as raw HTML.
This is standard PHP, not a Bolt feature. There is no component() function and nothing in the framework to call — a “component” is just an include plus PHP’s output buffering, wrapped in a naming convention. What Bolt contributes is the convention itself: where the partial lives (includes/components/), how it is named and guarded, and that its markup lands in the page buffer as raw HTML. The moment a partial needs its own instance-scoped CSS or JavaScript, you have outgrown this pattern — reach for a Block, which is real Bolt machinery: the block() function and its per-instance scoping.
Every component follows the same four-step lifecycle:
- Add the component to the
includes/components/folder. - Set a variable with the data the component needs.
- Include the component in the page.
- Render the result to the page as raw HTML.
1. Add the component to the includes/components folder
Create a .php file in includes/components/. A well-behaved component should:
- Read a single, predictably-named variable — a
$cardarray forfeature-card.php, a$calloutarray forcallout.php. - Guard that variable so the partial never fatals if a caller forgets to set it.
- Escape any dynamic text it prints with
htmlspecialchars(). unset()its input variable at the end, so stale data can't leak into a later include on the same page.
Here is a complete, reusable feature-card component. Save it as includes/components/feature-card.php:
<?php
// includes/components/feature-card.php — renders one feature card from a $card array.
//
// Usage:
// $card = [
// 'icon' => 'M12 6v6l4 2', // SVG path data
// 'title' => 'Fast routing',
// 'description' => 'File-based routing with zero configuration.',
// ];
// include ROOT_DIR . '/includes/components/feature-card.php';
// Guard: fall back to an empty array if the caller forgot to set $card.
$card = isset($card) && is_array($card) ? $card : [];
?>
<div css="background: var(--card); border: 1px_solid_var(--border); border-radius: var(--radius); padding: 1.5rem;">
<div css="width: 2.5rem; height: 2.5rem; border-radius: var(--radius); background: #dbeafe; display: flex; align-items: center; justify-content: center;">
<svg css="width: 1.25rem; height: 1.25rem; color: #2563eb;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="<?php echo htmlspecialchars($card['icon'] ?? ''); ?>"/>
</svg>
</div>
<h3 css="margin-top: 1rem; font-size: 1.125rem; font-weight: 600; color: #0f172a;"><?php echo htmlspecialchars($card['title'] ?? ''); ?></h3>
<p css="margin-top: 0.5rem; font-size: 0.875rem; color: #475569; line-height: 1.625;"><?php echo htmlspecialchars($card['description'] ?? ''); ?></p>
</div>
<?php unset($card); ?>
The partial is presentational only — it contains no business logic, no database calls, and no routing. Its sole job is to turn the $card array into markup.
2. Set a variable with data
On the page that needs the component, populate the variable the component expects. The keys you set must match the keys the component reads.
$card = [
'icon' => 'M13 10V3L4 14h7v7l9-11h-7z', // SVG path data for the icon
'title' => 'Lightning fast',
'description' => 'File-based routing with zero configuration.',
];
This is just a normal PHP variable. Build it however you like — hard-code it, pull it from the data store, or assemble it from a loop. The component neither knows nor cares where the data came from.
3. Include the component in the page
Use include with the ROOT_DIR constant so the path resolves correctly no matter which page or sub-directory you call it from. The component runs immediately, reads the $card you just set, and prints its markup at that point in the page.
<?php include ROOT_DIR . '/includes/components/feature-card.php'; ?>
Because pages build their body inside an ob_start() output buffer, the component's printed markup flows straight into that buffer — exactly as if you had typed the HTML inline.
To render the component more than once, set the variable again and include the file again:
<?php
$card = [
'icon' => 'M13 10V3L4 14h7v7l9-11h-7z',
'title' => 'Lightning fast',
'description' => 'File-based routing with zero configuration.',
];
include ROOT_DIR . '/includes/components/feature-card.php';
$card = [
'icon' => 'M12 6v6l4 2',
'title' => 'Always in sync',
'description' => 'Changes reload instantly in development.',
];
include ROOT_DIR . '/includes/components/feature-card.php';
?>
4. Render the result to the page as raw HTML
A Bolt page assembles its body into $page['content'] by closing the output buffer with ob_get_clean(). Everything the component printed is already captured in that buffer, so it becomes part of the string automatically.
<?php
$page['content'] = ob_get_clean();
?>
The layout performs the final render, echoing that string verbatim — as raw, un-escaped HTML — into the document:
<?php echo $page['content']; ?>
That single echo is where the component's markup actually reaches the browser. You never escape $page['content'] — it is HTML by design.
Capturing a component into a string
Sometimes you need a component's HTML as a value — to place it in a specific slot, pass it to another function, or render it conditionally — rather than printing it inline. Wrap the include in its own nested buffer and capture it:
<?php
// Build the component into a string instead of printing it inline.
$card = [
'icon' => 'M12 6v6l4 2',
'title' => 'Always in sync',
'description' => 'Changes reload instantly in development.',
];
ob_start();
include ROOT_DIR . '/includes/components/feature-card.php';
$cardHtml = ob_get_clean(); // $cardHtml now holds the rendered markup
// Render it wherever you need it — the string is already raw HTML:
echo $cardHtml;
?>
Output buffers nest, so capturing a component this way works even while the page's own ob_start() buffer is still open. Because $cardHtml is already fully-formed markup, you render it by echoing it directly — there is no escaping step.
Full example
A complete page that defines a list of features, then includes the component once per item with a foreach loop. The loop assigns $card on each pass, and the component reads it:
<?php
$page['config']['title'] = 'Features - Bolt CMS';
$page['config']['pageTitle'] = 'Features';
ob_start();
?>
<section css="padding-top: 4rem; padding-bottom: 4rem;">
<div css="max-width: 80rem; margin-left: auto; margin-right: auto; padding-left: 1rem sm:1.5rem lg:2rem; padding-right: 1rem sm:1.5rem lg:2rem;">
<div css="display: grid; grid-template-columns: repeat(1,minmax(0,1fr)) md:repeat(3,minmax(0,1fr)); gap: 1.5rem;">
<?php
$features = [
['icon' => 'M13 10V3L4 14h7v7l9-11h-7z', 'title' => 'Lightning fast', 'description' => 'File-based routing with zero configuration.'],
['icon' => 'M12 6v6l4 2', 'title' => 'Always in sync', 'description' => 'Changes reload instantly in development.'],
['icon' => 'M5 13l4 4L19 7', 'title' => 'Version controlled', 'description' => 'Every change is tracked in Git.'],
];
foreach ($features as $card) {
include ROOT_DIR . '/includes/components/feature-card.php';
}
?>
</div>
</div>
</section>
<?php
$page['content'] = ob_get_clean();
?>
The result is three feature cards, all rendered from one component file, with the page supplying nothing but data and layout.
Conventions
- One component, one file. Name the file after what it renders, in lowercase with hyphens:
feature-card.php,callout.php,profile-card.php. - Match the variable to the file.
feature-card.phpreads$card;callout.phpreads$callout. A predictable name makes every call site obvious. - Always guard the input. Open with
$card = isset($card) && is_array($card) ? $card : [];so a missing variable degrades gracefully instead of throwing. - Escape dynamic text. Wrap any value that prints into markup in
htmlspecialchars(). Pre-built HTML you intend to inject — like another captured component — is the deliberate exception. - Use
ROOT_DIRfor the path.include ROOT_DIR . '/includes/components/…'resolves from the project root, so the same call works from a top-level page or a nested sub-directory. - Keep components presentational. Fetch data and run logic in the page; let the component focus on markup.
unset()the input variable at the end of the component so its data cannot bleed into an unrelated include later on the same page.