ProcessWire
Recipes_

processwire-templates

What I Do

I provide comprehensive guidance for ProcessWire front-end template development:

  • Output strategies: direct, delayed, and markup regions
  • URL segments for routing within templates
  • Pagination with MarkupPagerNav
  • File includes and bootstrapping ProcessWire
  • Front-end rendering patterns and best practices

When to Use Me

Use this skill when:

  • Choosing an output strategy for a new site
  • Setting up template files with _init.php and _main.php
  • Using markup regions for flexible layouts
  • Implementing URL segments for custom routing
  • Adding pagination to search results or listings
  • Bootstrapping ProcessWire from external scripts

Output Strategies Overview

ProcessWire offers three main output strategies:

StrategyBest ForComplexity
Direct OutputSimple sites, single templatesLow
Delayed OutputComplex sites, multiple regionsMedium
Markup RegionsFlexible layouts, HTML-first approachLow-Medium

Direct Output

The simplest approach—output markup directly in template files.

Basic Example

/site/templates/basic-page.php:

<html>
<head>
    <title><?=$page->title?></title>
</head>
<body>
    <h1><?=$page->title?></h1>
    <?=$page->body?>
</body>
</html>

Using Include Files

Split common markup into reusable files:

/site/templates/_head.php:

<html>
<head>
    <title><?=$page->title?></title>
</head>
<body>
    <h1><?=$page->title?></h1>

/site/templates/_foot.php:

</body>
</html>

/site/templates/basic-page.php:

<?php
include("./_head.php");
echo $page->body;
include("./_foot.php");

Automatic File Includes

Configure in /site/config.php:

$config->prependTemplateFile = '_head.php';
$config->appendTemplateFile = '_foot.php';

Now template files only need the unique content:

<?php
echo $page->body;

Delayed Output

Populate variables first, output everything at the end.

Basic Structure

/site/templates/_init.php (prepended):

<?php
$headline = $page->get("headline|title");
$bodycopy = $page->body;
$sidebar = $page->sidebar;
$subnav = $page->children;

/site/templates/basic-page.php:

<?php
$bodycopy .= $page->comments->render();

/site/templates/_main.php (appended):

<!DOCTYPE html>
<html>
<head>
    <title><?=$headline?></title>
</head>
<body>
    <div id="bodycopy">
        <h1><?=$headline?></h1>
        <?=$bodycopy?>
    </div>
    <div id="sidebar">
        <?=$sidebar?>
        <?php if(count($subnav)): ?>
        <ul class="nav">
            <?php foreach($subnav as $child): ?>
            <li><a href="<?=$child->url?>"><?=$child->title?></a></li>
            <?php endforeach; ?>
        </ul>
        <?php endif; ?>
    </div>
</body>
</html>

Configuration

// /site/config.php
$config->prependTemplateFile = '_init.php';
$config->appendTemplateFile = '_main.php';

Using region() Function

Alternative to variables—IDE-friendly and always in scope:

// _init.php - define with default
region('bodycopy', $page->body);

// basic-page.php - populate
region('bodycopy', "<h2>$page->headline</h2>" . $page->body);

// _main.php - output
echo region('bodycopy');

Enable with:

$config->useFunctionsAPI = true;

Markup Regions

HTML-based approach combining direct output simplicity with delayed output power.

Enable Markup Regions

// /site/config.php
$config->useMarkupRegions = true;
$config->appendTemplateFile = '_main.php';

Alternative ID Attributes

Use pw-id or data-pw-id instead of id for invisible region markers (removed from final output, hidden from CSS/JS):

<!-- These are equivalent, but pw-id/data-pw-id won't appear in rendered HTML -->
<div id="content">...</div>
<div pw-id="content">...</div>
<div data-pw-id="content">...</div>

How It Works

  1. Region definitions: HTML tags with id attributes in _main.php
  2. Region actions: Template files output tags with same IDs to populate/modify regions

Region Definition Example

/site/templates/_main.php:

<!DOCTYPE html>
<html lang="en">
  <head id="html-head">
    <meta charset="utf-8" />
    <title id="html-title"><?=$page->title?></title>
  </head>
  <body id="html-body">
    <div id="masthead">
      <ul id="topnav">
        <?php foreach($pages->get('/')->children as $item): ?>
        <li>
          <a href="<?=$item->url?>"><?=$item->title?></a>
        </li>
        <?php endforeach; ?>
      </ul>
    </div>
    <div id="content">
      <h1 id="headline"><?=$page->title?></h1>
      <div id="bodycopy"><?=$page->body?></div>
      <div id="sidebar" pw-optional></div>
    </div>
    <div id="footer">
      <p>Copyright <?=date('Y')?></p>
    </div>
  </body>
</html>

Region Action Attributes

AttributeBehavior
pw-replaceReplace region content (default)
pw-appendAppend to region
pw-prependPrepend to region
pw-beforeInsert before region
pw-afterInsert after region

Note: All pw-* attributes also work with data-pw-* prefix (e.g., data-pw-replace, data-pw-append). These are removed from final output.

Placeholder Tags

Use <region> or <pw-region> tags for regions where only inner content should appear in output (wrapper tag is removed):

<!-- Definition in _main.php - wrapper tag won't appear in output -->
<region id="sidebar">
  <p>Default sidebar content</p>
</region>

<!-- Action in template -->
<pw-region id="sidebar">
  <h3>Custom Title</h3>
  <p>Custom content</p>
</pw-region>

<!-- Final output - no wrapper tag, only inner content -->
<h3>Custom Title</h3>
<p>Custom content</p>

Populating Regions

/site/templates/basic-page.php:

Replace content:

<div id="bodycopy">
  <p>This replaces the default bodycopy content.</p>
</div>

Append to region (outer HTML):

<ul class="subnav" pw-append="sidebar">
  <?=$page->children->each("
  <li><a href="{url}">{title}</a></li>
  ")?>
</ul>

Prepend to region (inner HTML only):

<div id="sidebar" pw-prepend>
  <h3>Sidebar Title</h3>
</div>

Insert after element:

<h2 pw-after="headline"><?=$page->summary?></h2>

Add to head:

<link rel="stylesheet" href="/custom.css" pw-append="html-head" />

Optional Regions

Use pw-optional for regions that should be removed if empty:

<div id="sidebar" pw-optional></div>

Adding/Removing Classes

Classes merge automatically:

<!-- Definition -->
<ul id="mylist" class="foo">
  ...
</ul>

<!-- Action - adds "bar" class -->
<ul id="mylist" class="bar" pw-append>
  <li>New item</li>
</ul>

<!-- Result -->
<ul id="mylist" class="foo bar">
  ...
</ul>

Remove class with minus prefix:

<ul id="mylist" class="-foo bar" pw-append>
  ...
</ul>

Debugging Regions

Add this comment anywhere in your <html>...</html> to see debug output:

<!--PW-REGION-DEBUG-->

Output format:

3. replace => #content-head ... <h1 id='content-head'>
4. replace => #sidebar ... <aside id='sidebar'>
5. replace => #content-body ... <div class='uk-margin-top' id='content-body'>
   0.0044 seconds

Format: number. action => region-id ... tag followed by processing time.

Performance Considerations

Markup regions add processing overhead compared to direct/delayed output. For most sites this is negligible, but avoid markup regions if your site outputs very large amounts of markup.

When to Use Markup Regions

Best for:

  • Developers who like direct output simplicity but want delayed output benefits
  • Front-end developers who prefer HTML-first approach
  • New ProcessWire users

Consider alternatives if:

  • You already have a working output strategy
  • Your site handles very heavy markup output

URL Segments

Enable template files to act as URL routers.

Enable URL Segments

  1. Go to Setup > Templates > [template] > URLs
  2. Check “Allow URL Segments”

Or configure max segments in /site/config.php:

$config->maxUrlSegments = 4;  // default

Accessing URL Segments

For URL /products/hammer/photos/gallery/:

$input->urlSegment1;      // "photos"
$input->urlSegment2;      // "gallery"
$input->urlSegment(1);    // "photos"
$input->urlSegmentStr;    // "photos/gallery"
$input->urlSegmentStr();  // "photos/gallery"

Routing Example

// Throw 404 if more than 1 segment
if(strlen($input->urlSegment2)) throw new Wire404Exception();

switch($input->urlSegment1) {
    case '':
        // Main content (no segment)
        echo $page->body;
        break;

    case 'photos':
        // Photo gallery
        include('./_photos.php');
        break;

    case 'map':
        // Location map
        include('./_map.php');
        break;

    default:
        // Unknown segment - 404
        throw new Wire404Exception();
}

Using urlSegmentStr

Check multiple segments at once:

if($input->urlSegmentStr === 'photos/primary') {
    // Primary photo
} else if($input->urlSegmentStr === 'photos/secondary') {
    // Secondary photo
} else if(strlen($input->urlSegmentStr)) {
    throw new Wire404Exception();
}

URL Segment Whitelist

Define allowed segments in template settings (Setup > Templates > URLs) to automatically 404 on unknown segments.


Pagination

Display large result sets across multiple pages.

Enable Pagination

  1. Install MarkupPagerNav module (Modules > Core > Markup)
  2. Enable for template: Setup > Templates > [template] > URLs > “Allow Page Numbers”

Basic Usage

$results = $pages->find("template=product, limit=10, sort=title");

// Render results with automatic pagination
echo $results->render();

// Or render just pagination links
echo $results->renderPager();

Custom Pagination

$results = $pages->find("template=blog-post, limit=10, sort=-date");
$pagination = $results->renderPager();

echo $pagination;  // Top pagination

echo "<ul class='posts'>";
foreach($results as $post) {
    echo "<li>";
    echo "<h2><a href='{$post->url}'>{$post->title}</a></h2>";
    echo "<p>{$post->summary}</p>";
    echo "</li>";
}
echo "</ul>";

echo $pagination;  // Bottom pagination

Current Page Number

$pageNum = $input->pageNum;  // Current page number (1-based)

echo "<h1>Results (Page $pageNum)</h1>";

Pagination Options

echo $results->renderPager([
    'numPageLinks' => 10,
    'nextItemLabel' => 'Next &raquo;',
    'previousItemLabel' => '&laquo; Prev',
    'listMarkup' => "<ul class='pagination'>{out}</ul>",
    'itemMarkup' => "<li class='{class}'>{out}</li>",
    'linkMarkup' => "<a href='{url}'>{out}</a>",
    'currentItemClass' => 'active',
    'separatorItemLabel' => '...',
]);

Prevent Auto-Pagination

Use start=0 to prevent automatic pagination adjustment:

// Always get first 10 results, regardless of page number
$featured = $pages->find("featured=1, start=0, limit=10");

Pagination CSS

.MarkupPagerNav {
  margin: 1em 0;
  padding: 0;
}
.MarkupPagerNav li {
  display: inline-block;
  margin: 0 2px;
}
.MarkupPagerNav li a {
  display: block;
  padding: 5px 10px;
  background: #eee;
  text-decoration: none;
}
.MarkupPagerNav li.MarkupPagerNavOn a,
.MarkupPagerNav li a:hover {
  background: #333;
  color: #fff;
}

Bootstrapping ProcessWire

Use ProcessWire’s API from external PHP scripts.

Basic Bootstrap

<?php
include("/path/to/processwire/index.php");

// API is now available
$products = $pages->find("template=product");
foreach($products as $product) {
    echo $product->title . "\n";
}

With Namespace (PW 3.x)

<?php namespace ProcessWire;

include("/path/to/processwire/index.php");

$contact = $pages->get("/about/contact/");
echo $contact->address;

Command-Line Script

#!/usr/bin/php
<?php namespace ProcessWire;

include("/var/www/site/index.php");

// Generate sitemap
function listPage($page, $level = 0) {
    echo str_repeat("  ", $level) . $page->title . "\n";
    foreach($page->children as $child) {
        listPage($child, $level + 1);
    }
}

listPage($pages->get("/"));

Alternative API Access

// All equivalent after bootstrap
$page = $pages->get("/about/");
$page = pages("/about/");
$page = wire('pages')->get("/about/");
$page = $wire->pages->get("/about/");

File Include Functions

wireIncludeFile()

Include file with variable isolation:

wireIncludeFile('./_header.php', [
    'title' => $page->title,
    'showNav' => true
]);

wireRenderFile()

Render file and return as string:

$header = wireRenderFile('./_header.php', [
    'title' => $page->title
]);

echo $header;

Common Patterns

Conditional Layouts

// _init.php
$layout = '_main.php';

if($page->template == 'admin-page') {
    $layout = '_admin.php';
} else if($page->template == 'ajax-handler') {
    $layout = '';  // No layout wrapper
}

// basic-page.php
if($layout) include($layout);

Template-Specific Includes

// Include template-specific file if it exists
$customFile = "./_custom/{$page->template}.php";
if(file_exists($config->paths->templates . ltrim($customFile, './'))) {
    include($customFile);
}

JSON API Response

// api-endpoint.php
header('Content-Type: application/json');

$data = [
    'title' => $page->title,
    'body' => $page->body,
    'children' => []
];

foreach($page->children as $child) {
    $data['children'][] = [
        'title' => $child->title,
        'url' => $child->url
    ];
}

echo json_encode($data);
exit;  // Skip _main.php

Pitfalls / Gotchas

  1. Prepend/append order matters:

    • prependTemplateFile runs before your template
    • appendTemplateFile runs after your template
    • Variables set in prepend are available in template and append
  2. Markup regions require <html> tag:

    • Region actions must be output before <html>
    • Region definitions must be inside <html>...</html>
  3. URL segments vs page names:

    • Real child pages take precedence over URL segments
    • Avoid segment names that might conflict with page names
  4. Pagination and template caching:

    • Works with caching (up to 999 pages cached)
    • GET/POST vars are not cached—don’t use template cache for search results
  5. Exit to skip appended files:

    // For JSON/AJAX responses
    echo json_encode($data);
    exit;  // Prevents _main.php from loading
  6. Debug mode for regions:

    • Add <!--PW-REGION-DEBUG--> to see what’s happening
    • Use <!--#regionid--> after closing tags to help PW find region ends