Page Surfaces
Every public and client page resolves to one view file inside your theme. This is the complete map from address to file.
Overview
A controller never names a template file. It names a view, a slash-separated path with no extension. The engine turns that into a file inside your theme: render("account/services") becomes views/account/services.tpl for a Smarty theme and views/account/services.twig for a Twig one. The theme decides the extension through its manifest; the controller never learns which engine is running.
That single rule is what makes a theme portable. Ship the 69 view files a full theme needs and every surface answers. Ship fewer and the controller answers 404 for the ones you left out, because almost every website controller checks the view first.
Structure
The Views Tree
Views are grouped by area, never dumped flat into one folder. The counts below are the shipped themes measured file by file. Basic and WStyle carry 69 views each; WCOM carries 110, because it adds a large set of file-based marketing pages.
| Folder | Basic / WStyle | What lives there |
|---|---|---|
views/ (root) | 6 | Standalone surfaces with no natural family: home, domain search, the SMS landing page, licence verification, 404, maintenance |
views/products/ | 3 | Catalog: category page, software detail page, software store |
views/checkout/ | 14 | Configure, cart, checkout, payment and their shared fragments |
views/account/ | 25 | The signed-in client area, from the dashboard to sub accounts |
views/auth/ | 6 | Login, register, password reset, activation, invitation |
views/content/ | 14 | Editorial surfaces: blog, news, references, knowledge base, CMS pages, contact |
views/page/ | 1 | File-based pages: a view here publishes a URL with no controller and no route |
Layouts, Partials and Components
Views hold page bodies only. The shell around them lives in three sibling folders, referenced from the theme root, never relative to the view.
default (public and client shell), auth (split stage and form), checkout (focused funnel shell), invoice (print-friendly).
header, topbar, drawer, footer, announcements, cookie-notice. Client area: client-topbar, client-sidebar, client-footer. Funnel: checkout-header, checkout-footer.
Reference
The Resolution API
Three methods decide whether a surface exists and how it is produced. All three are on the theme instance returned by Theme::active().
public static function active(): self;
// True when views/<view>.<ext> is a real file. $view is slash separated, no extension.
// '' and any path containing '..' return false before the disk is touched.
public function viewExists(string $view): bool;
// Render one view to a string, independent of request context (cron, mail, PDF).
public function render(string $view, array $data = []): string;
// Read one theme setting: config.php value, or the schema default from theme.php.
public function setting(string $key): mixed;
// The manifest's meta block, including the theme flags the core reads.
public function meta(): array;
// Absolute URL under the theme's assets/ folder. Backs the {asset} template function.
public function assetUrl(string $path = ''): string;
twig gives .twig, php gives .php, anything else gives .tpl. Website controllers alone call it 41 times, and every one of those guards a page.
View::chose("website") silently skips the theme when CRON or ADMINISTRATOR is defined.
filter:template.variables, loads the view's content scope and finally filters the finished markup.
views/.
Root Views
| View | Produced by | Address |
|---|---|---|
home | controllers/website/index.php | route home, the site root |
domain | controllers/website/domain.php | route domain, /domain |
sms-introduction | controllers/website/sms.php | route international-sms, /international-sms |
license-verification | controllers/website/license.php | route license, /license-verify |
404 | Controllers::page_404 | no route: any unmatched address, and every failed view guard |
maintenance | controllers/system/maintenance.php | no route: every address while maintenance mode is on |
Catalog Views
| View | Produced by | Address |
|---|---|---|
products/category | controllers/website/products.php | routes products and products-2, /{category} and /{kind}/{category} |
products/software | controllers/website/softwares.php | routes softwares and softwares_cat, /softwares and /softwares/{category} |
products/detail | controllers/website/product-detail.php | route software_detail, /software/{slug} |
Checkout Views
Four of the fourteen are fragments. They carry no layout, and another template pulls them in or an AJAX response returns them.
| View | Produced by | Address |
|---|---|---|
checkout/configure | controllers/website/configure.php | routes configure, configure-p, /configure/{type}/{id} |
checkout/configure-addon | configure.php, page_addon() | the addon branch of the same configure address |
checkout/configure-domain | configure.php, page_edit_item() | route configure-edit, /configure/edit/{key} |
checkout/cart | controllers/website/cart.php | routes cart and basket, /cart |
checkout/checkout | controllers/website/checkout.php | route checkout, /checkout |
checkout/order-complete | checkout.php, page_complete() | route order-complete, /checkout/complete/{id} |
checkout/invoice-complete | invoices.php, page_complete() | route invoice-complete, /invoices/complete/{id} |
checkout/pay | controllers/website/pay.php | route pay, /pay/{id} |
checkout/pay-result | controllers/website/payment.php | routes pay-successful and pay-failed |
checkout/pay-choices | operations ClientCheckout, ClientInvoicePay, BalanceOps | fragment: AJAX response, three callers share it |
checkout/section-account | checkout/checkout.tpl | fragment: included by the checkout body |
checkout/section-billing | checkout/checkout.tpl | fragment: included by the checkout body |
checkout/section-payment | checkout/checkout.tpl | fragment: included by the checkout body |
checkout/section-rail-items | checkout/checkout.tpl | fragment: included by the order summary rail |
Client Area Views
| View | Produced by | Address |
|---|---|---|
account/dashboard-hero | controllers/website/dashboard.php | route my-account, /dashboard, hero layout variant |
account/dashboard-standard | dashboard.php | same address, standard layout variant |
account/settings | controllers/website/account.php | route info, /account/info |
account/services | services.php, page_list() | route services, /services |
account/service-detail | services.php, page_detail() | route service-detail, /services/detail/{id} |
account/service-transfer-approve | services.php, page_transfer_approve() | route service-transfer-approve, token in the path |
account/domains | domains.php, page_list() | route domains, /domains |
account/domain-detail | domains.php, page_detail() | route domain-detail, /domain-detail/{id} |
account/invoices | invoices.php, page_list() | route invoices, /invoices |
account/invoice-detail | invoices.php, page_detail() | route invoice-detail, /invoices/detail/{id} |
account/subscriptions | invoices.php, page_subscriptions() | route invoice-subscriptions, /invoices/subscriptions |
account/bulk-pay | invoices.php, page_bulk_pay() | route bulk-pay, /invoices/bulk-pay |
account/balance | controllers/website/balance.php | route balance, /balance |
account/tickets | tickets.php, page_list() | route tickets, /tickets |
account/ticket-detail | tickets.php, page_detail() and page_guest_view() | routes ticket-detail and ticket-guest |
account/ticket-create | tickets.php, page_create() | route ticket-create, /tickets/create |
account/ticket-msg | tickets.php | fragment: one reply, returned to the AJAX poller |
account/sms | sms.php, page_panel() | route sms, /sms |
account/sub-accounts | controllers/website/sub-accounts.php | route sub-accounts, /sub-accounts |
account/api-credentials | controllers/website/api.php | route api, /api-credentials |
account/affiliate | controllers/website/affiliate.php | route affiliate, /affiliate |
account/reseller-program | reseller.php, page_program() | route reseller, guest and non reseller view |
account/reseller | reseller.php, page_dashboard() | route reseller, the reseller's own panel |
account/license-transfer-verify | verify-license-transfer.php | route verify-license-transfer, token in the path |
account/access-denied | Controllers, the permission guard | no route: any client page a sub account may not open |
Auth Views
All six go through one private helper on the sign controller. That is why they share a guard, a canonical link and a page title convention.
| View | Produced by | Address |
|---|---|---|
auth/login | sign.php, page_in() | route sign-in, /login |
auth/register | sign.php, page_up() | route sign-up, /register |
auth/forget-password | sign.php, page_forget() | route sign-forget, /forget-password |
auth/reset-password | sign.php, page_reset() | route sign-reset, /reset-password |
auth/activate | sign.php, page_activate() | route account-activation, /account-activation |
auth/accept-invite | sign.php, page_invite() | route accept-invite, /invitation/{token} |
Content Views
| View | Produced by | Address |
|---|---|---|
content/blog | controllers/website/articles.php | route articles, /articles |
content/blog-detail | page-detail.php | route articles_detail, a bare slug |
content/blog-comment-list | operation ClientBlogComments | fragment: the comment list, reloaded over AJAX |
content/news | controllers/website/news.php | route news, /news |
content/news-detail | page-detail.php | route news_detail, a bare slug |
content/references | controllers/website/references.php | route references, /references |
content/references-detail | page-detail.php | route references_detail, a bare slug |
content/page-detail | page-detail.php | routes normal_detail and contract_detail, every CMS page and contract |
content/knowledgebase | knowledgebase.php | route kbase, /knowledgebase |
content/knowledgebase-category | knowledgebase.php | route kbase_category, /knowledgebase/category/{slug} |
content/knowledgebase-article | knowledgebase.php and knowledgebase_detail.php | route kbase_detail, /knowledgebase/article/{slug} |
content/contact | controllers/website/contact.php | route contact, /contact |
content/newsletter-unsubscribe | controllers/website/newsletter.php | no route key: the first URL segment resolves to the controller |
content/addon | controllers/website/addon.php | no route key: /addon/{ModuleName}, an addon module's own page |
File-Based Pages
A view under views/page/ publishes an address on its own. No controller, no route entry, no core edit. A routing filter runs last, after every registered route has failed to match, and hands the request to a generic controller when the file is there.
/about. The slug is restricted to a-z 0-9 / _ - and any .. is rejected, checked twice: once in the routing filter, once in the controller.
page/ prefix does not reach the locale: views/page/about.tpl reads locale/{lang}/about.php, because the scope mirrors the public URL, not the folder.
What Happens When a View Is Missing
Three different things, depending on who asked. Knowing which one you are looking at saves an hour of hunting for a template error that was never raised.
| Caller | Behaviour | What you see |
|---|---|---|
A controller that guards with viewExists() | Returns the 404 page instead | HTTP 404 with your theme's 404 view, no error at all |
| A view with no guard | The engine throws, the dispatcher catches it and returns an empty string | A blank page. A warning goes to the log, and in development the message appears inside a preformatted block |
| An optional feature view | The feature turns itself off | No error, a different code path runs (see the PDF pitfall below) |
Example
One surface end to end: the controller line that names the view, and the view file that answers it. The two halves are shown together because the view path is the only contract between them.
// controllers/website/services.php, page_list()
// Guard first: a theme that ships no services list answers 404 rather than a blank page.
if (!Theme::active()->viewExists("account/services")) return $this->page_404("website");
$this->addData("page_title", Language::gc("website/services/meta/title"));
$this->addData("canonical_link", LinkGenerator::client("services"));
$this->addData("meta_robots", "noindex, nofollow");
// Turns on the client shell (sidebar, subnav, notification bell) and marks the active tab.
$this->addData("show_client_subnav", true);
$this->addData("subnav_active", "services");
// Logo, company name, contract links, currency list, the client chrome data.
$this->set_predefined_data("client");
// No extension, no engine: "account/services" is resolved against the ACTIVE theme.
return $this->view->chose("website")->render("account/services", $this->data, true);
{extends file='layouts/default.tpl'}
{* Stylesheets and libraries the core JS itself consumes go in head. *}
{block name=head}
<script src="{asset path='js/list.js'}" defer></script>
{/block}
{* Page scripts go in scripts, AFTER the core bundle, or window.WStyle is not ready yet. *}
{block name=scripts}
<script src="{asset path='js/services.js'}" defer></script>
{/block}
{block name=content}
<section class="pt-4 pb-4">
<div class="container list-page" data-services-page>
{csrf form='services'}
<nav aria-label="{lang key='website/services/breadcrumb-aria'}">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{link route='my-account'}">{lang key='website/index/subnav-dashboard'}</a></li>
<li class="breadcrumb-item active" aria-current="page">{lang key='website/services/title'}</li>
</ol>
</nav>
</div>
</section>
{/block}
Pitfalls
The engine dispatcher catches every throwable and returns an empty string in production. If a surface comes out as nothing at all, look for a view file before you look at your data. In development the caught message appears on the page instead, which is the fastest way to confirm it.
View::chose("website") only resolves the theme when neither CRON nor ADMINISTRATOR is defined. From a scheduled task it silently falls back to plain PHP templates. It then fails to find your template file and returns an empty string. Use Theme::active()->render() for cron, mail attachments and PDF generation.
The controller first asks for account/dashboard. If that file exists the theme has one dashboard and the layout setting is ignored entirely. Only when it is absent does the controller fall back to account/dashboard-hero or account/dashboard-standard, picked by the theme setting. WCOM ships the single view, Basic and WStyle ship the pair.
Invoice PDF generation picks headless Chrome only when a Chrome binary resolves and the theme ships account/invoice-pdf. None of the three bundled themes ships it, so the built in generator is what actually runs today. Adding that one file changes the output engine for every invoice.
It is the one view that does not extend a layout. The site is closed, so no navigation, cart or account chrome may link back into it. It opens with its own doctype, its own head and its own first-paint guard. A theme that ships no maintenance view falls back to the legacy system template, which is not skinned.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.