Core $pages/$page API methods, the selector query language, and a from-scratch setup walkthrough for ProcessWire's page-tree-first CMS.
| 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->title | Reads (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->parent | Returns 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
Structure
Sort & Limit
Status & Access
Every clause is comma-separated and reads left to right — this finds the 10 newest published blog-post pages tagged "news".
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.
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.
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.
Delete the installer
Remove install.php and the site/install/ directory once setup completes — leaving it live is a known attack surface.
Quick Tips
addHookAfter()/addHookBefore() in site/ready.php to extend behavior. Core updates then stay a simple file overwrite.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
Delayed Output
Markup Regions
Partials
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.
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.
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.
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.
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
/site/config.php's $config->pagePathHistory and template-level URL segments handle anything more custom.Installing the Twig Stack
Require the module via Composer
Run from the site root — pulls in TemplateEngineFactory and the Twig library it wraps.
Install both modules in Admin
Modules → site → refresh, then install TemplateEngineFactory first, followed by the TemplateEngineTwig engine module it depends on.
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.
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.
|raw is required for HTML fields|raw (or the engine's autoescape setting adjusted) or it prints as literal tags on the page.{% 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.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 |
|---|---|---|
| Template | Admin → Templates → "blog-post" | ProcessWire's own Template record — defines which fields a page of this type has. |
| Controller | controllers/BlogPost.php | Optional. Runs before rendering; sets extra variables the view needs beyond the automatic ones. |
| View | views/blog-post.twig | The Twig file actually rendered — receives every variable the controller set, plus the automatic ones. |
page
pages
config
user
Twig Syntax Quick Reference
| Syntax | Purpose | Example |
|---|---|---|
| Output | Print a variable or expression | {{ page.title }} |
| Condition | If / else branching | {% if page.images %}...{% else %}...{% endif %} |
| Loop | Iterate a PageArray or list | {% for child in page.children %}...{% endfor %} |
| Filter | Transform a value inline | {{ page.summary|striptags|slice(0, 120) }} |
| Raw HTML | Disable auto-escaping for a field | {{ page.body|raw }} |
| Extends | Inherit a layout's shell | {% extends "layouts/default.twig" %} |
| Block | Define/override a named region | {% block content %}...{% endblock %} |
| Include | Pull in a partial | {% include "partials/sidebar.twig" %} |
| Set | Declare a local variable | {% set featured = pages.find('featured=1') %} |
| Comment | Non-rendered note | {# TODO: swap in real image #} |
{% set news = pages.find('template=news-item, limit=5, sort=-date') %} — selectors aren't a PHP-only feature.-> operator — $page->title in PHP becomes page.title, and Twig tries property, then method, then array key automatically.{{ dump(page) }} in any view to inspect what's actually available — faster than guessing field names.