Views & Templates
TrueFramework's view engine is plain PHP. Templates are .phtml files with a small metadata block at the top, wrapped at render time in a layout you control.
Render a template
$App->view->render('contact.phtml', [
'firstName' => $firstName,
'errors' => $errors,
]);
The variables array is unpacked into the template's local scope — inside contact.phtml you'll see $firstName and $errors directly.
Template metadata
Anything between the start of the file and a {endmeta} marker is parsed as INI-style metadata, not template body:
title = "Contact Us"
description = "Get in touch with our team."
canonical = "https://example.com/contact"
modified = "2026-04-30"
css = "/assets/css/contact.css"
js = "/assets/js/contact.js"
{endmeta}
<h1>Contact</h1>
Anything declared here is available on $App->view inside the layout — $App->view->title, $App->view->description, etc.
Meta tags (Open Graph, Twitter Cards, etc.)
Beyond the standard title, description and keywords keys, any other key you put in the view's meta header is turned into a <meta> tag in the <head> automatically. Prefix the key with property:, name:, http-equiv:, or link: to choose the attribute used — the prefix is stripped from the output and only controls which attribute is emitted. A plain key with no prefix becomes a <meta name="..."> tag.
title = "This is the title of the page"
description = "This is the meta description of the page."
property:og:type = "website"
property:og:title = "This is the title of the page"
property:og:url = "{url}"
property:og:description = "This is the meta description of the page."
property:og:image = "https://www.domain.com/image.jpg"
property:og:image:width = "1200"
property:og:image:height = "630"
property:og:site_name = "The Site Name"
property:og:locale = "en_US"
name:twitter:card = "summary_large_image"
name:twitter:url = "{url}"
name:twitter:title = "This is the title of the page"
name:twitter:description = "This is the meta description of the page."
{endmeta}
Choosing the attribute
| Prefix | Output |
|---|---|
property:og:title | <meta property="og:title" content="..."> — Open Graph (og:*, fb:*, article:*) |
name:twitter:card | <meta name="twitter:card" content="..."> — Twitter Cards and other name-based tags |
http-equiv:refresh | <meta http-equiv="refresh" content="..."> |
link:author | <link rel="author" href="..."> — emits a <link> rather than a <meta> |
(no prefix) robots | <meta name="robots" content="..."> — defaults to the name attribute |
Open Graph tags require the property attribute (not name), so always prefix them with property:.
Placeholders
Meta values support a handful of placeholders that are filled in at render time, so you don't have to hard-code per-page URLs:
| Placeholder | Replaced with |
|---|---|
{url} | The current request URL |
{canonical} | The canonical value if set, otherwise the current URL |
{title} | The page title |
{description} | The page description |
{image} | The og:image value if one was set |
Automatic defaults
To save repeating common tags on every page, a few Open Graph tags are filled in automatically when you don't supply them:
og:typedefaults towebsite.og:titleis copied from the pagetitleif you didn't set anog:title.og:descriptionis copied from the pagedescriptionif you didn't set anog:description.og:urlis set to thecanonicalvalue, or the current URL when no canonical is defined.
Anything you set explicitly always wins. Duplicate keys are de-duplicated by attribute + name — setting the same tag twice keeps the last value rather than emitting it twice.
These tags are part of the head output, so the base template must echo it inside <head> (TrueFramework base templates already do):
<?=$App->view->headOutput?>
Canonical URL
The canonical meta key sets the page's <link rel="canonical"> tag, which tells search engines the preferred URL for the page. You can hard-code an absolute URL, but the easiest approach is to use the {url} placeholder, which is replaced with the current request URL at render time:
title = "This is the title of the page"
canonical = "{url}"
{endmeta}
That outputs:
<link rel="canonical" href="https://www.example.com/about">
If you don't set canonical at all, the framework still adds a canonical link using the accessed URL — so canonical = "{url}" mainly makes that behavior explicit. Set it to a fixed URL instead when several paths serve the same page and you want them to consolidate on one address:
canonical = "https://www.example.com/about"
{endmeta}
The canonical value also feeds the {canonical} placeholder and the automatic og:url default, so setting it once keeps your canonical link and Open Graph URL in sync.
Last-Modified and the timezone key
PhpView sends a Last-Modified HTTP header for every page so browsers and caches can revalidate it. By default the date comes from the view file's own modification time. Override it with the modified meta key, and use the timezone key to tell the framework which timezone that date is written in — the value is then converted to GMT for the header.
title = "This is the title of the page"
modified = "2026-01-12 09:30:00"
timezone = "America/Los_Angeles"
{endmeta}
timezoneaccepts any valid PHP timezone identifier (America/New_York,Europe/London,UTC, …).- If you set
modifiedbut omittimezone, the date is parsed with the server's default timezone. - If you omit
modifiedentirely, the view file's last-changed time is used.
Output the modified date in the page body with the modified() helper, which formats the stored date with any PHP date format string:
<p>Last updated: <?=$App->view->modified('F j, Y')?></p>
Created date
The created meta key records the date a page was first published. Unlike modified it does not affect any HTTP headers — it is purely a value you can display in the page (a "Published on" line, or a datePublished field in JSON-LD).
title = "This is the title of the page"
created = "2026-06-01"
{endmeta}
Output it with the created() helper, passing any PHP date format string:
<p>Published: <?=$App->view->created('F j, Y')?></p>
If you don't set created, the helper falls back to today's date.
Author and Label
author is an ordinary unprefixed key — it is emitted as <meta name="author" content="..."> in the head, the conventional way to declare authorship.
title = "The Complete Guide to Anxiety Therapy"
author = "Author Name, Title"
{endmeta}
label is a short display name for the page, available to layouts and partials as $App->view->label. It produces no HTML output on its own — navigation helpers or admin UIs use it when they need a title shorter than the full title tag.
title = "The Complete Guide to Anxiety Therapy"
label = "Anxiety Therapy"
{endmeta}
Layouts
The layout wraps every rendered view. Set it once in your bootstrap or per-request:
$App->view->layout = BP.'/app/views/_layouts/base.phtml';
A typical layout pulls metadata from $App->view and yields the rendered body via $App->view->bodyHtml (the variable name depends on your template — the renderer sets it for you):
<!doctype html>
<html>
<head>
<title><?=esc($App->view->title ?? '')?></title>
<meta name="description" content="<?=esc($App->view->description ?? '')?>">
<?= $App->view->headHtml ?? '' ?>
</head>
<body>
<?= $App->view->bodyHtml ?>
</body>
</html>
Asset hooks
Multiple comma-separated paths can be assigned to a single declaration. The layout iterates them and emits <link> / <script> tags:
// In a controller, before render()
$App->view->css = '/assets/css/page.css';
$App->view->js = '/assets/js/page.js, /assets/js/extra.js';
// Append more without overwriting
$App->view->js = '/assets/js/analytics.js';
Layout output helpers
Three short calls in your layout do most of the per-page injection work. Drop them in the obvious places — CSS in <head>, errors near the top of <body>, JS just before </body>:
<!doctype html>
<html>
<head>
<title><?=esc($App->view->title ?? '')?></title>
<?=$App->view->cssoutput?>
<?=$App->view->headHtml ?? ''?>
</head>
<body>
<?php $App->displayErrors(); ?>
<?=$App->view->bodyHtml?>
<?=$App->view->jsoutput?>
</body>
</html>
What each one does:
-
<?=$App->view->cssoutput?>— emits the<link>tag for every CSS path declared in your template'scss =metadata header or assigned via$App->view->css = '...'. The framework reads each file from disk, runs a minifier across the concatenated result, writes it topublic_html/assets/css/cache/<hash>.css, and outputs a single<link rel="stylesheet">. The cache key is derived from the file list, so changing the order or set of files invalidates it; editing the contents of a listed file does not — delete the cached file (or change the list) to refresh. -
<?php $App->displayErrors(); ?>— drains any messages queued during the request via$App->error($msg, 'notice'|'warning'|'error')and renders them as a native<dialog>modal with an OK button. The first message becomes the headline; additional messages stack below it. Output happens inline at the point you call it, so put it somewhere it can render markup. If nothing was queued it prints nothing. -
<?=$App->view->jsoutput?>— the JS counterpart tocssoutput. Local paths declared injs =metadata (or assigned via$App->view->js = '...') are concatenated and cached topublic_html/assets/js/cache/<hash>.js, then emitted as one<script>. Entries that look like CDN URLs (contain://) or wildcards keep their own<script>tag so they're not pulled into the bundle.
Both cssoutput and jsoutput are populated when the view renders, so call them in the layout (which runs after the view body) — not before render() in a controller.
Inline blocks
The metadata header supports a few special blocks for one-off page-level injection:
{head}
<meta property="og:type" content="article">
{/head}
{style}
.callout { color: red }
{/style}
{script}
console.log('loaded');
{/script}
{jsonld}
{"@context":"https://schema.org","@type":"Article"}
{/jsonld}
{endmeta}
JSON-LD structured data
For your own structured data in the <head>, wrap raw JSON in a {jsonld}...{/jsonld} block in the view's meta header. The contents are emitted verbatim inside a <script type="application/ld+json"> tag, so you write schema.org JSON exactly as you want it to appear.
title = "This is the title of the page"
description = "This is the meta description of the page."
{jsonld}
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "This is the title of the page",
"author": { "@type": "Person", "name": "Author Name" },
"publisher": { "@type": "Organization", "name": "Company Name" },
"datePublished": "2026-01-01",
"dateModified": "2026-01-12",
"description": "This is the meta description of the page."
}
{/jsonld}
{endmeta}
- You can include more than one
{jsonld}block in the same page — each becomes its own<script type="application/ld+json">tag. - Placeholders like
{url},{canonical},{title},{description}and{image}are filled in inside the block, so you can reuse page values without repeating them. - The block must be valid JSON; it is emitted as-is and is not validated for you.
- This is independent of the breadcrumb
BreadcrumbListJSON-LD (below), which is generated automatically — both can appear on the same page.
For arbitrary head markup that isn't JSON-LD (extra <link>, verification tags, etc.), use a {head}...{/head} block the same way — its contents are injected into the <head> unchanged.
Breadcrumbs
PhpView builds SEO breadcrumbs for a page from a single set of meta entries. From one definition it produces two things:
- A visible breadcrumb trail you can drop anywhere in the page body with the
tag. - A matching
BreadcrumbListJSON-LD block, generated automatically and injected into the<head>for search engines.
Defining the crumbs
Add breadcrumb[] entries to the view's meta header. Each entry is a Label|/url pair. The breadcrumb[] array syntax lets you list as many levels as you need, in order from the top of the site down to the current page.
title = "Anxiety Therapy"
description = "How we help with anxiety."
breadcrumb[] = "Therapy Services|/services"
breadcrumb[] = "Anxiety"
{endmeta}
<h1>Anxiety Therapy</h1>
The last crumb is normally the current page, so leave off its |/url part. A crumb with no URL renders as plain text in the trail; in the JSON-LD it automatically uses the current page's URL (from the canonical meta if set, otherwise the requested URL).
Home is implied
You do not need to add a Home crumb. Unless your first entry already links to /, a Home → / crumb is prepended automatically:
Home > Therapy Services > Anxiety
If you want a different label or want to control it yourself, make the first entry point at /:
breadcrumb[] = "Start|/"
breadcrumb[] = "Therapy Services|/services"
breadcrumb[] = "Anxiety"
{endmeta}
Outputting the visible trail
Place the tag anywhere in the body of the view (or in a partial). It is replaced with the rendered trail. If no breadcrumb[] entries were defined, the tag becomes an empty string — safe to leave in a shared template.
{endmeta}
<h1>Anxiety Therapy</h1>
The generated HTML — linked crumbs use the URLs you supplied; the final crumb is plain text:
<div id="breadcrumbs"><a href="/">Home</a> > <a href="/services">Therapy Services</a> > Anxiety</div>
Style it via the #breadcrumbs id.
Changing the separator
The separator defaults to > (a > character). Set your own once, typically in init.php so it applies site-wide:
$App->view->setBreadcrumbsDelimiter('/');
Any string works ('/', '»', '›', etc.). The method returns the view object so it can be chained.
JSON-LD for search engines
When breadcrumbs are defined, a BreadcrumbList structured data block is generated automatically and added to the head output. URLs are converted to absolute (using the current scheme and host), and the final link-less crumb is resolved to the current page URL. You do not call anything to enable this — it happens whenever breadcrumb[] entries are present.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Therapy Services", "item": "https://www.example.com/services" },
{ "@type": "ListItem", "position": 3, "name": "Anxiety", "item": "https://www.example.com/anxiety-therapy" }
]
}
</script>
Site-wide meta defaults
setDefaults() registers a single INI file whose values are applied to every page before that page's own {endmeta} block is parsed. Set a site-wide og:site_name, a default social image, or any other tag once and let individual views override it.
// typically in init.php, right after creating the view
$App->view->setDefaults('metadata-defaults.ini');
Paths follow the usual config rule: a leading / is treated as absolute; anything else is resolved relative to BP."/app/config/". So 'metadata-defaults.ini' looks for BP/app/config/metadata-defaults.ini.
The defaults file uses the same INI format as a view's meta header. Every key supported in a view is supported here — standard keys (title, description, keywords, canonical, css, js, cache, label, etc.), prefixed meta/link keys (property:og:*, name:twitter:*, http-equiv:*, link:*), and breadcrumb[] entries.
; app/config/metadata-defaults.ini
property:og:site_name = "My Site"
property:og:locale = "en_US"
property:og:image = "https://www.example.com/assets/images/social-default.jpg"
name:twitter:card = "summary_large_image"
Values from the defaults file load first; anything a view sets in its own {endmeta} block wins. Automatic defaults (og:title from title, og:url from canonical) still apply on top of both layers and only fill in tags that remain unset after both passes.
Globals
Pass values you want available to every view through $App->view->variables:
$App->view->variables = [
'siteName' => 'Acme',
'year' => (int) date('Y'),
];
Configuration
Override defaults at construction time:
$App->view = new \True\PhpView([
'base_path' => BP.'/app/views/',
'assets_path' => '/assets/',
'layout' => BP.'/app/views/_layouts/base.phtml',
'404' => '404-error.phtml',
'cache' => false,
]);