Page Surfaces

4 Aufrufe Markdown

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.

FolderBasic / WStyleWhat lives there
views/ (root)6Standalone surfaces with no natural family: home, domain search, the SMS landing page, licence verification, 404, maintenance
views/products/3Catalog: category page, software detail page, software store
views/checkout/14Configure, cart, checkout, payment and their shared fragments
views/account/25The signed-in client area, from the dashboard to sub accounts
views/auth/6Login, register, password reset, activation, invitation
views/content/14Editorial surfaces: blog, news, references, knowledge base, CMS pages, contact
views/page/1File-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.

layouts/ Full documents a view extends. WStyle ships four: default (public and client shell), auth (split stage and form), checkout (focused funnel shell), invoice (print-friendly).
partials/ Chrome the layout includes, in three sets. Public: header, topbar, drawer, footer, announcements, cookie-notice. Client area: client-topbar, client-sidebar, client-footer. Funnel: checkout-header, checkout-footer.
components/ Reusable blocks a view includes with arguments: plan grids, dashboard panels, payment method lists, configure sub cards.
tables/ Plain PHP table presets for the client lists: services, domains, invoices, tickets. The list engine reads them, not a template.

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().

exact signatures
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;
Theme::viewExists() The extension comes from the manifest engine: 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.
Theme::render() The context free entry point. Use it from cron, mail and PDF code; View::chose("website") silently skips the theme when CRON or ADMINISTRATOR is defined.
View::render() The in-request path used by every controller. It adds the address variables, runs filter:template.variables, loads the view's content scope and finally filters the finished markup.
TemplateEngine::render_file() The engine dispatcher underneath both. It builds a fresh Smarty or Twig instance per call and prefixes the view with views/.

Root Views

ViewProduced byAddress
homecontrollers/website/index.phproute home, the site root
domaincontrollers/website/domain.phproute domain, /domain
sms-introductioncontrollers/website/sms.phproute international-sms, /international-sms
license-verificationcontrollers/website/license.phproute license, /license-verify
404Controllers::page_404no route: any unmatched address, and every failed view guard
maintenancecontrollers/system/maintenance.phpno route: every address while maintenance mode is on

Catalog Views

ViewProduced byAddress
products/categorycontrollers/website/products.phproutes products and products-2, /{category} and /{kind}/{category}
products/softwarecontrollers/website/softwares.phproutes softwares and softwares_cat, /softwares and /softwares/{category}
products/detailcontrollers/website/product-detail.phproute 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.

ViewProduced byAddress
checkout/configurecontrollers/website/configure.phproutes configure, configure-p, /configure/{type}/{id}
checkout/configure-addonconfigure.php, page_addon()the addon branch of the same configure address
checkout/configure-domainconfigure.php, page_edit_item()route configure-edit, /configure/edit/{key}
checkout/cartcontrollers/website/cart.phproutes cart and basket, /cart
checkout/checkoutcontrollers/website/checkout.phproute checkout, /checkout
checkout/order-completecheckout.php, page_complete()route order-complete, /checkout/complete/{id}
checkout/invoice-completeinvoices.php, page_complete()route invoice-complete, /invoices/complete/{id}
checkout/paycontrollers/website/pay.phproute pay, /pay/{id}
checkout/pay-resultcontrollers/website/payment.phproutes pay-successful and pay-failed
checkout/pay-choicesoperations ClientCheckout, ClientInvoicePay, BalanceOpsfragment: AJAX response, three callers share it
checkout/section-accountcheckout/checkout.tplfragment: included by the checkout body
checkout/section-billingcheckout/checkout.tplfragment: included by the checkout body
checkout/section-paymentcheckout/checkout.tplfragment: included by the checkout body
checkout/section-rail-itemscheckout/checkout.tplfragment: included by the order summary rail

Client Area Views

ViewProduced byAddress
account/dashboard-herocontrollers/website/dashboard.phproute my-account, /dashboard, hero layout variant
account/dashboard-standarddashboard.phpsame address, standard layout variant
account/settingscontrollers/website/account.phproute info, /account/info
account/servicesservices.php, page_list()route services, /services
account/service-detailservices.php, page_detail()route service-detail, /services/detail/{id}
account/service-transfer-approveservices.php, page_transfer_approve()route service-transfer-approve, token in the path
account/domainsdomains.php, page_list()route domains, /domains
account/domain-detaildomains.php, page_detail()route domain-detail, /domain-detail/{id}
account/invoicesinvoices.php, page_list()route invoices, /invoices
account/invoice-detailinvoices.php, page_detail()route invoice-detail, /invoices/detail/{id}
account/subscriptionsinvoices.php, page_subscriptions()route invoice-subscriptions, /invoices/subscriptions
account/bulk-payinvoices.php, page_bulk_pay()route bulk-pay, /invoices/bulk-pay
account/balancecontrollers/website/balance.phproute balance, /balance
account/ticketstickets.php, page_list()route tickets, /tickets
account/ticket-detailtickets.php, page_detail() and page_guest_view()routes ticket-detail and ticket-guest
account/ticket-createtickets.php, page_create()route ticket-create, /tickets/create
account/ticket-msgtickets.phpfragment: one reply, returned to the AJAX poller
account/smssms.php, page_panel()route sms, /sms
account/sub-accountscontrollers/website/sub-accounts.phproute sub-accounts, /sub-accounts
account/api-credentialscontrollers/website/api.phproute api, /api-credentials
account/affiliatecontrollers/website/affiliate.phproute affiliate, /affiliate
account/reseller-programreseller.php, page_program()route reseller, guest and non reseller view
account/resellerreseller.php, page_dashboard()route reseller, the reseller's own panel
account/license-transfer-verifyverify-license-transfer.phproute verify-license-transfer, token in the path
account/access-deniedControllers, the permission guardno 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.

ViewProduced byAddress
auth/loginsign.php, page_in()route sign-in, /login
auth/registersign.php, page_up()route sign-up, /register
auth/forget-passwordsign.php, page_forget()route sign-forget, /forget-password
auth/reset-passwordsign.php, page_reset()route sign-reset, /reset-password
auth/activatesign.php, page_activate()route account-activation, /account-activation
auth/accept-invitesign.php, page_invite()route accept-invite, /invitation/{token}

Content Views

ViewProduced byAddress
content/blogcontrollers/website/articles.phproute articles, /articles
content/blog-detailpage-detail.phproute articles_detail, a bare slug
content/blog-comment-listoperation ClientBlogCommentsfragment: the comment list, reloaded over AJAX
content/newscontrollers/website/news.phproute news, /news
content/news-detailpage-detail.phproute news_detail, a bare slug
content/referencescontrollers/website/references.phproute references, /references
content/references-detailpage-detail.phproute references_detail, a bare slug
content/page-detailpage-detail.phproutes normal_detail and contract_detail, every CMS page and contract
content/knowledgebaseknowledgebase.phproute kbase, /knowledgebase
content/knowledgebase-categoryknowledgebase.phproute kbase_category, /knowledgebase/category/{slug}
content/knowledgebase-articleknowledgebase.php and knowledgebase_detail.phproute kbase_detail, /knowledgebase/article/{slug}
content/contactcontrollers/website/contact.phproute contact, /contact
content/newsletter-unsubscribecontrollers/website/newsletter.phpno route key: the first URL segment resolves to the controller
content/addoncontrollers/website/addon.phpno 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.

views/page/about.tpl Published at /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.
filter:routing.match The router's last fallback. It never shadows a registered route, so adding a file-based page can never break an existing address.
locale scope The 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.

CallerBehaviourWhat you see
A controller that guards with viewExists()Returns the 404 page insteadHTTP 404 with your theme's 404 view, no error at all
A view with no guardThe engine throws, the dispatcher catches it and returns an empty stringA blank page. A warning goes to the log, and in development the message appears inside a preformatted block
An optional feature viewThe feature turns itself offNo 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.

the controller side
// 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);
views/account/services.tpl, the view that answers it
{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

A missing view is a blank page, not an exception

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.

Background output must not go through the request path

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 dashboard is one view or two, and the theme decides

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.

An optional view can switch a whole engine on

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.

The maintenance view is a whole document, not a page body

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.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.