Back to All Cheatsheet Libraries cheatsheets

ProcessWire

Core $pages/$page API methods, the selector query language, and a from-scratch setup walkthrough for ProcessWire's page-tree-first CMS.

Total Methods: 0
Object Method / Property Description
$pages$pages->find('selector')Returns a PageArray of every page matching the selector.
$pages$pages->get('selector')Returns a single matching Page (or NullPage if nothing matches).
$pages$pages->save($page)Saves a Page object and all its field values back to the database.
$pages$pages->trash($page)Moves a page to the trash rather than deleting it outright.
$pages$pages->newPage('template')Creates a new, unsaved Page instance using the given template.
$page$page->titleReads (or, in a template context, writes) the page's title field.
$page$page->children('selector')Returns this page's direct children, optionally filtered by a selector.
$page$page->parentReturns the page's parent Page object.
$page$page->render()Outputs the page rendered through its template file — handy for AJAX partials.
$page$page->of(false)Turns off "output formatting" so field values return raw/editable rather than formatted for display.
$fields$fields->get('name')Returns a Field definition object by name.
$templates$templates->get('name')Returns a Template definition object by name.
$user$user->isLoggedin()Checks whether the current visitor is authenticated.
$user$user->hasRole('role-name')Checks whether the current user has a given permission role.
$input$input->get('name')Sanitized read of a GET query parameter (ProcessWire sanitizes by default, unlike raw $_GET).
$input$input->post('name')Sanitized read of a POST field.
$session$session->redirect('/path/')Issues an HTTP redirect and halts execution.
$sanitizer$sanitizer->text($str)Strips markup/newlines from a string — the standard way to clean any user input before use.

Selector Operators

ProcessWire's selectors are its own compact query language — used identically in $pages->find(), template access control, and Lister filters.

Comparison

= equals != not equal > / < greater/less %= LIKE contains

Structure

template= parent= children.count id=

Sort & Limit

sort=field sort=-field limit=10 start=20

Status & Access

status=published status!=hidden include=all check_access=0
$pages->find("template=blog-post, categories=news, sort=-date, limit=10");

Every clause is comma-separated and reads left to right — this finds the 10 newest published blog-post pages tagged "news".

New Site, Start to Finish

ProcessWire ships as a single download with its own installer — no separate CLI tool required to get running.

1

Download & extract

Pull the latest core from processwire.com or GitHub and extract it into the web root. No Composer/build step is required for a standard install.

2

Run the web installer

Visiting the site in a browser launches install.php, which checks PHP/MySQL requirements, writes site/config.php, and creates the admin superuser.

3

Design the page tree first

ProcessWire is page-tree-first: define your Templates and Fields, then build the tree structure — the front end is just PHP files rendering whatever page matches the URL.

4

Delete the installer

Remove install.php and the site/install/ directory once setup completes — leaving it live is a known attack surface.

rm install.php && rm -rf site/install/

Quick Tips

Hooks over core edits
Never modify core files — use addHookAfter()/addHookBefore() in site/ready.php to extend behavior. Core updates then stay a simple file overwrite.
Modules directory
Most "is there a plugin for X" needs are answered at modules.processwire.com before writing custom code.
Fields are reusable
A Field is defined once and can be attached to many Templates — rename/reconfigure it centrally rather than duplicating similar fields per template.

Site Directory Structure

/site/templates/

One PHP file per Template — basic-page.php renders any page using the "basic-page" Template. This is where front-end markup lives.

/site/templates/_main.php

Convention (not required) for a shared wrapper included by every template file — header, footer, and the main HTML shell.

/site/templates/_init.php / _func.php

Common convention for bootstrap code and helper functions auto-prepended before every template renders, configured in Admin → Templates → Files.

/site/assets/

Generated/cached files (image variations, compiled CSS, session data) — safe to delete, ProcessWire regenerates it.

/site/modules/

Custom and third-party modules — each in its own subfolder with a matching classname.module.php file.

/site/config.php

Site-specific configuration (DB credentials, debug mode, timezone) — kept out of version control via .gitignore in most setups.

Template File Conventions

Direct Output

PHP echoes HTML inlineSimplest, most common

Delayed Output

Build $content string_main.php echoes it later

Markup Regions

HTML with region tagsAppend/prepend/replace blocks

Partials

wireIncludeFile()Reusable component chunks

Building a Custom Theme, Start to Finish

ProcessWire has no separate "theme" concept like WordPress — the site's templates ARE the theme. This is the practical build order.

1

Define Templates before writing PHP

In Admin → Templates, create the Template names your site actually needs (home, basic-page, blog-post) — each gets an empty matching .php file in /site/templates/ automatically.

2

Build the shared shell first

Write _main.php with the full HTML document — header, nav, footer — and a single echo $content; in the body, so every other template can focus on just its own markup.

3

Fill in per-template markup

Each template file reads its own fields ($page->title, $page->body, custom fields) and assigns rendered HTML to $content rather than echoing directly, under the Delayed Output pattern.

4

Add CSS/JS as static assets

No build step is required — link stylesheets/scripts from /site/templates/styles/ or similar directly in _main.php, or wire up a bundler if the project calls for one.

<link rel="stylesheet" href="<?= $config->urls->templates ?>styles/main.css">
5

Package as a site profile (optional)

A finished site's /site/ directory can be zipped as a reusable "site profile" — ProcessWire's own installer can bootstrap a brand new install directly from one.

Theming Tips

Reuse across Templates with Fieldsets
A Fieldset groups related fields (e.g. SEO title/description) so multiple Templates can share the same block without redefining each field.
URL structure follows the page tree
There's no separate routes file by default — a page's URL is its position in the tree, though /site/config.php's $config->pagePathHistory and template-level URL segments handle anything more custom.
No enforced front-end framework
Plain PHP + any CSS/JS approach works — Tailwind, Bootstrap, or hand-written CSS are equally common across ProcessWire sites, unlike CMSes with an opinionated theme layer.

Comprehensive Twig Theme Builder

ProcessWire ships with plain-PHP templates by default, but the community-standard way to theme with Twig is TemplateEngineFactory + its TemplateEngineTwig engine module (by Wanze). Everything below assumes that stack — the one-stop reference for main layout, sidebar, partials, and Twig syntax.

Installing the Twig Stack

1

Require the module via Composer

Run from the site root — pulls in TemplateEngineFactory and the Twig library it wraps.

composer require wanze/template-engine-factory
2

Install both modules in Admin

Modules → site → refresh, then install TemplateEngineFactory first, followed by the TemplateEngineTwig engine module it depends on.

3

Set Twig as the active engine

In TemplateEngineFactory's module config, set "Template engine" to Twig, and confirm the three directory settings — Views, Layouts, Partials — described below.

4

Create the directory structure

Under /site/templates/, add the three folders the module expects.

Directory Layout

/site/templates/views/

One .twig file per ProcessWire Template — basic-page.twig is auto-rendered for any page using the "basic-page" Template, mirroring the plain-PHP .php convention.

/site/templates/layouts/

Base page shells (e.g. default.twig) that hold the <html>/<head>/<body> skeleton and define named {% block %} regions for views to fill in.

/site/templates/partials/

Reusable fragments — header, footer, sidebar, nav — pulled into any view or layout via {% include %}.

/site/templates/controllers/

Optional PHP controller per Template (e.g. BasicPage.php) that runs before its matching view renders, for prepping data beyond what's available automatically.

The Main Page Layout

The layout is the single shell every page extends — this is the direct Twig equivalent of a PHP theme's _main.php wrapper.

layouts/default.twig

Defines the document shell and the named regions child views are allowed to fill.

<!DOCTYPE html> <html lang="en"> <head> <title>{% block title %}{{ page.title }}{% endblock %}</title> <link rel="stylesheet" href="{{ config.urls.templates }}styles/main.css"> </head> <body> {% include "partials/header.twig" %} <div class="layout-grid"> <main>{% block content %}{% endblock %}</main> <aside>{% block sidebar %}{% include "partials/sidebar.twig" %}{% endblock %}</aside> </div> {% include "partials/footer.twig" %} </body> </html>

views/basic-page.twig

Extends the layout and only fills in the blocks it needs to override — everything else falls back to the layout's defaults.

{% extends "layouts/default.twig" %} {% block content %} <h1>{{ page.title }}</h1> <div class="body-copy">{{ page.body|raw }}</div> {% endblock %}
|raw is required for HTML fields
Twig auto-escapes output by default — a ProcessWire Textarea/CKEditor field's HTML must be piped through |raw (or the engine's autoescape setting adjusted) or it prints as literal tags on the page.
Blocks can have defaults
Anything placed between a layout's {% block x %}...{% endblock %} tags is the fallback content — a view that doesn't override the block just inherits it, as done above with the sidebar block.

Sidebar & Reusable Partials

Partials are plain Twig files with no {% extends %} of their own — they're pulled into a layout or view wherever needed via {% include %}.

partials/sidebar.twig

A typical dynamic sidebar built from a ProcessWire page selector, rendered the same way on every page that includes it.

<nav class="sidebar-nav"> <h3>{{ sidebarTitle|default('More in this section') }}</h3> <ul> {% for child in page.rootParent.children %} <li class="{{ child == page ? 'is-active' : '' }}"> <a href="{{ child.url }}">{{ child.title }}</a> </li> {% endfor %} </ul> </nav>

Plain include

{% include "partials/sidebar.twig" %} — reuses whatever variables are already in scope (e.g. page).

Include with data

{% include "partials/sidebar.twig" with {'sidebarTitle': 'Related Posts'} %} — passes extra variables the partial can use, without polluting the parent scope.

Isolated include

Add only after the with {...} clause to give the partial only the variables listed — nothing else leaks in from the parent template.

Optional include

{% include "partials/promo.twig" ignore missing %} — silently skips rendering if the file doesn't exist, useful for optional per-section blocks.

Header & footer are partials too
Treat partials/header.twig and partials/footer.twig exactly like the sidebar — one file, included from the layout, so nav/branding changes happen in a single place.
Nest partials freely
A partial can itself {% include %} another partial (e.g. sidebar including a "newsletter-box" partial) — keep each one focused on a single UI chunk.

Template ↔ View ↔ Controller Mapping

TemplateEngineFactory auto-matches a ProcessWire Template to a Twig view of the same name — a Controller is an optional PHP step in between for prepping data.

Piece File Role
TemplateAdmin → Templates → "blog-post"ProcessWire's own Template record — defines which fields a page of this type has.
Controllercontrollers/BlogPost.phpOptional. Runs before rendering; sets extra variables the view needs beyond the automatic ones.
Viewviews/blog-post.twigThe Twig file actually rendered — receives every variable the controller set, plus the automatic ones.

controllers/BlogPost.php

A controller class extends the factory's base ViewController and hands data to the view with set().

<?php namespace ProcessWire; class BlogPost extends \TemplateEngineFactory\ViewController { public function init() { $related = $this->wire('pages')->find( "template=blog-post, id!={$this->wire('page')->id}, limit=3" ); $this->view->set('relatedPosts', $related); } }

page

Auto-availableCurrent $page object

pages

Auto-available$pages API for selectors

config

Auto-available$config, e.g. config.urls

user

Auto-availableCurrently logged-in $user

Twig Syntax Quick Reference

Syntax Purpose Example
OutputPrint a variable or expression{{ page.title }}
ConditionIf / else branching{% if page.images %}...{% else %}...{% endif %}
LoopIterate a PageArray or list{% for child in page.children %}...{% endfor %}
FilterTransform a value inline{{ page.summary|striptags|slice(0, 120) }}
Raw HTMLDisable auto-escaping for a field{{ page.body|raw }}
ExtendsInherit a layout's shell{% extends "layouts/default.twig" %}
BlockDefine/override a named region{% block content %}...{% endblock %}
IncludePull in a partial{% include "partials/sidebar.twig" %}
SetDeclare a local variable{% set featured = pages.find('featured=1') %}
CommentNon-rendered note{# TODO: swap in real image #}
ProcessWire selectors still work
Twig views can call the full PW API — {% set news = pages.find('template=news-item, limit=5, sort=-date') %} — selectors aren't a PHP-only feature.
Field access uses dot notation
Twig has no -> operator — $page->title in PHP becomes page.title, and Twig tries property, then method, then array key automatically.
Debug with dump()
Enable Twig's debug extension in module config, then use {{ dump(page) }} in any view to inspect what's actually available — faster than guessing field names.