---
title: Views & Smarty
description: How .tpl templates are resolved, the View::assign pipeline, caching, and shared partials.
---

Views in Nibiru are **Smarty 3** templates. Each controller has a default template; nested actions can have their own. The `View` singleton wraps the Smarty engine and exposes one global helper: `View::assign()`.

## Where templates live

```
application/view/
├── templates/                # source .tpl files (you write these)
│   ├── index.tpl             # → indexController::pageAction()
│   ├── products.tpl          # → productsController::pageAction()
│   ├── products/
│   │   └── detail.tpl        # → productsController::detailAction()
│   ├── shared/
│   │   ├── header.tpl
│   │   ├── footer.tpl
│   │   └── meta.tpl
│   ├── navigation.tpl
│   └── pageination.tpl       # (note the spelling)
├── templates_c/              # Smarty compile cache (auto)
├── cache/                    # rendered HTML cache (when caching=true)
└── mockup/                   # static design mockups
```

`templates_c/` is created and managed by Smarty. **It must be writable by your web server user.** `cache/` is only used when `[ENGINE] caching = true`.

## Resolution rules

The Display layer resolves the template path from the matched controller:

- For `pageAction()` → `templates/<controller>.tpl`.
- For a named action `fooAction()` → `templates/<controller>/foo.tpl` if it exists, otherwise `templates/<controller>.tpl`.

If neither resolves, the [soft 404](/core/dispatcher/#soft-404) error template is rendered.

## A minimal template

```smarty
{include 'shared/header.tpl'}
<body>
{include file="navigation.tpl"}

<main class="container">
  <h1>{$title|escape}</h1>
  <ul>
    {foreach $products as $p}
      <li><a href="/products/detail/{$p.id}">{$p.name|escape}</a></li>
    {/foreach}
  </ul>
</main>

{include 'shared/footer.tpl'}
</body>
```

`{include 'shared/header.tpl'}` and `{include file="navigation.tpl"}` are both valid Smarty include forms.

## View::assign

`View::assign()` is how PHP data reaches templates. It's static, idempotent, and array-friendly:

```php
View::assign(['title' => 'Products']);
View::assign([
    'products' => $list,
    'count'    => count($list),
]);
```

Later calls overwrite earlier ones for the same key. Inside templates these become `{$title}`, `{$products}`, `{$count}`.

There's no `View::display()` you call manually — the dispatcher invokes `Display::display()` automatically after `pageAction()` returns.

## Shared CSS/JS injection

The convention used across the showcase apps is to push the configured asset list into the template:

```php
View::assign([
    'css' => Config::getInstance()->getConfig()[View::NIBIRU_SETTINGS]['smarty.css'],
    'js'  => Config::getInstance()->getConfig()[View::NIBIRU_SETTINGS]['smarty.js'],
]);
```

```smarty
{* shared/header.tpl *}
<head>
  <meta charset="utf-8">
  <title>{$title|escape}</title>
  {foreach $css as $stylesheet}
    <link rel="stylesheet" href="{$stylesheet}">
  {/foreach}
</head>
```

Then `[SETTINGS] smarty.css[]` in the INI file is the single source of truth for stylesheets.

## Caching

Caching is opt-in:

```ini
[ENGINE]
caching = true
```

When enabled, Smarty caches the rendered HTML in `application/view/cache/` with the lifetime `Smarty::CACHING_LIFETIME_CURRENT` (default ≈ 1 hour). Clear the cache with:

```bash
./nibiru -cache-clear
```

This wipes both `templates_c/` and `cache/` safely.

:::caution
Cached pages don't run your controller. Don't cache pages with user-specific content or CSRF tokens.
:::

## Smarty essentials

Things you'll reach for daily:

```smarty
{* variables *}
{$user.name}
{$products[0].title}

{* iteration *}
{foreach $items as $item}
  ...
{foreachelse}
  No items.
{/foreach}

{* conditionals *}
{if $count > 0}…{else}…{/if}

{* string filters *}
{$body|escape}              {* HTML escape *}
{$date|date_format:"%Y-%m-%d"}
{$price|string_format:"%.2f"}

{* includes *}
{include file="shared/header.tpl" title=$title}

{* assigning *}
{assign var="now" value=time()}
```

## JSON responses

When an action calls `View::forwardToJsonHeader()`, Smarty is bypassed and Nibiru emits the assigned `data` key as JSON. Handy for AJAX endpoints:

```php
public function searchAction() {
    View::forwardToJsonHeader();
    $results = $this->index->search($_REQUEST['q'] ?? '');
    View::assign(['data' => $results]);
}
```

## Navigation includes

`{include file="navigation.tpl"}` reads from the JSON file configured in `[SETTINGS] navigation`. Production apps often load **multiple** named nav arrays:

```php
public function navigationAction() {
    JsonNavigation::getInstance()->loadJsonNavigationArray('headnavigation');
    JsonNavigation::getInstance()->loadJsonNavigationArray('mainnavigation');
    JsonNavigation::getInstance()->loadJsonNavigationArray('footer');
}
```

Each named array becomes a Smarty variable of the same name, ready to render in the matching partial.

## Common gotchas

- **`templates_c/` permissions** — if Smarty can't write there, you'll get a fatal error. Run `./nibiru -s` once to fix.
- **`{$variable}` with no value** — Smarty in default mode renders nothing rather than throwing. Turn on `error_reporting = E_ALL` and `[ENGINE] debug = true` while developing to spot typos.
- **HTML escaping** — Smarty doesn't auto-escape. Use `|escape` for any user-controlled string.
