You launched an e-commerce store on ProcessWire, but pages take 3 seconds to load and LCP is off the charts? The default theme adds tons of unused scripts, and the markup isn't adaptable to your data. A custom template solves this – you get exactly what you need, no bloat. With 8 years of ProcessWire experience, we guarantee a reliable solution. Switching to custom templates reduces TTFB by 2-3x by eliminating unused modules.
How to Develop a Custom ProcessWire Template?
A template in ProcessWire is a PHP file in /site/templates/, whose name matches the template name in the database. The file receives the $page object with current page data and full access to $pages, $config, $user, and the rest of the API. Pure PHP – no default engine, though you can integrate Twig via the TemplateEngineTwig module. For production we recommend pure PHP: it's faster and adds no extra abstraction layer.
Why Is a Custom Template Better Than a Standard Theme?
Standard themes are often overloaded with unused scripts and styles, increasing LCP and CLS. A custom template outputs only needed data, uses server-side rendering for heavy components, and implements lazy loading for images. In one of our projects – a ProcessWire e-store with 5000+ products – switching to custom templates reduced memory consumption by 60% and sped up page generation by 2x. Additionally, we cut database queries from 50 to 3 per page.
Template Architecture: Splitting Options
Option 1: _init.php + _main.php
The most common approach. ProcessWire automatically includes _init.php before each template and _main.php after, if the "Prepend/Append template file" option is enabled in the template settings:
// _init.php и _main.php — единый поток $baseUrl = $config->urls->root; $homePage = $pages->get("/"); $mainNav = $pages->find("parent=/,template!=error404,sort=sort"); ?><!DOCTYPE html> <html lang="<?= $user->language->name ?>"> <head> <meta charset="utf-8"> <title><?= $page->title ?> | <?= $homePage->title ?></title> <link rel="stylesheet" href="<?= $config->urls->templates ?>css/main.css"> </head> <body> <?php include("partials/nav.php"); ?> <main><?= $content ?></main> <?php include("partials/footer.php"); ?> </body> </html> Option 2: Controller + View
For complex templates, logic is extracted into a separate controller file. This simplifies maintenance and testing:
// templates/services.php — точка входа require __DIR__ . '/controllers/services.php'; extract($viewData); require __DIR__ . '/views/services.view.php'; // templates/controllers/services.php — контроллер $viewData = [ 'items' => $pages->find("template=service, sort=sort, limit=12"), 'categories' => $pages->find("template=service-category, sort=sort"), 'pagination' => $modules->get("MarkupPagerNav"), ]; Approach Comparison
| Feature | init/main | controller+view |
|---|---|---|
| Complexity | Low | Medium |
| Maintainability | Simple | Flexible |
| Testability | Low | High |
| Performance | High | High |
| Recommendation | Small projects | Complex projects |
Working with Images
ProcessWire processes images on the fly via the size() method. Example with WebP:
$img = $page->images->first(); if ($img) { $thumb = $img->size(800, 600, ['cropping' => 'center', 'quality' => 85]); echo "<img src='{$thumb->url}' alt='{$img->description}' loading='lazy'>"; $webp = $img->size(800, 600, ['suffix' => 'webp', 'webpAdd' => true]); } Custom 404 and Redirects
if ($page->redirect_url) { $session->redirect($page->redirect_url, 301); } if (!$user->isLoggedin() && $page->requires_login) { throw new Wire404Exception(); } How to Optimize Images: Step-by-Step
- Select an image from the page gallery.
- Call
$img->size()with desired parameters. - Specify
suffix: 'webp'andwebpAdd: trueto generate WebP. - Add the
loading='lazy'attribute for deferred loading. - Use
$pages->findMany()for bulk conversion on high-traffic sites.
Performance: Lazy Loading Relationships
By default, FieldtypePage loads related objects upon access. For large lists, use $pages->findMany() – stream processing without loading everything into memory:
foreach ($pages->findMany("template=product, sort=title") as $product) { echo $product->title . "\n"; } Details about findMany()
findMany() processes pages one by one, not storing them in memory. This is especially important on pages with thousands of products. It works as a generator: each iteration loads only the current page.Typical Template Development Timelines
| Task | Estimate |
|---|---|
| Basic template (layout + nav + footer) | 4–8 h |
| List template with pagination and filter | 8–16 h |
| Detailed page with relationships | 4–10 h |
| Template system for 10+ content types | 3–6 days |
| Twig integration + component approach | 2–4 days |
What's Included in Development?
- Template architecture (init/main or controller+view)
- Configuration of prepend/append files
- Markup for all typical pages (list, detail, 404)
- Image optimization (size, WebP, lazy loading)
- Basic SEO markup (meta title, description, og:image)
- Template and access documentation
- Client team training (1 hour online)
- 3-month code warranty
Training and documentation are separate items: we transfer knowledge so you can maintain the project independently.
Typical Problems and Solutions
Not using output buffering with prepend/append leads to duplicate headers. Solution – always enable prepend/append or use the controller pattern. Loading all related pages at once – for sets over 100 records use $pages->findMany(). Ignoring image caching – ProcessWire caches variants, but if content changes frequently, clear the cache in /site/assets/files/. A 404 page is mandatory: always throw Wire404Exception for protected sections.
Contact us for a free consultation – we'll discuss your project. Order custom ProcessWire template development and get flexibility and performance.
MDN Lazy loading guide – more about the loading attribute. Wikipedia Core Web Vitals – performance metrics.







