Bolt CMS Docs
Sign in

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:

  1. Add the component to the includes/components/ folder.
  2. Set a variable with the data the component needs.
  3. Include the component in the page.
  4. 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 $card array for feature-card.php, a $callout array for callout.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.php reads $card; callout.php reads $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_DIR for 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.
Design system Blocks