2026-02-07 19:14:13 +01:00
# Templates
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
Templates control how content is presented. FolderWeb uses plain PHP templates — HTML with embedded PHP for dynamic output.
2025-11-27 23:01:02 +01:00
## Template Types
FolderWeb has three template levels:
2026-02-07 19:14:13 +01:00
### Base Template (`base.php`)
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
The HTML scaffold wrapping every page. Contains `<head>` , navigation, footer, and a `$content` placeholder for the inner template.
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
### Page Template (`page.php`)
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
Wraps single-page content. Receives the rendered content and metadata.
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
### List Templates (`list.php`, `list-grid.php`, `list-compact.php`)
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
Displays collections of items from subdirectories. Receives an `$items` array and optional intro content.
2025-11-27 23:01:02 +01:00
## Template Location
2026-02-07 19:14:13 +01:00
Templates live in `custom/templates/` :
2025-11-27 23:01:02 +01:00
```
custom/
└── templates/
├── base.php # HTML scaffold
├── page.php # Single page wrapper
├── list.php # Default list layout
├── list-grid.php # Grid card layout
└── list-compact.php # Compact list layout
```
2026-02-07 19:14:13 +01:00
FolderWeb falls back to `app/default/templates/` for any template not present in `custom/` .
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
## Base Template
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
The base template wraps every page. Here is a minimal example:
2025-11-27 23:01:02 +01:00
```php
<!DOCTYPE html>
< html lang = "<?= $currentLang ?? 'en' ?>" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > <?= htmlspecialchars($pageTitle) ?> < / title >
2026-02-07 19:14:13 +01:00
2025-11-27 23:01:02 +01:00
<?php if (isset($metaDescription)): ?>
< meta name = "description" content = "<?= htmlspecialchars($metaDescription) ?>" >
<?php endif; ?>
<?php if (isset($socialImageUrl)): ?>
< meta property = "og:image" content = "<?= $socialImageUrl ?>" >
<?php endif; ?>
< link rel = "stylesheet" href = "/custom/styles/base.css?v=<?= $cssHash ?? '' ?>" >
2026-02-07 19:14:13 +01:00
2025-11-27 23:01:02 +01:00
<?php if (isset($pageCssUrl)): ?>
< link rel = "stylesheet" href = "<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>" >
<?php endif; ?>
< / head >
< body >
< header >
< nav >
2026-02-07 19:14:13 +01:00
< a href = "<?= htmlspecialchars($langPrefix ?? '') ?>/" > My Site< / a >
2025-11-27 23:01:02 +01:00
< ul >
<?php foreach ($navigation as $item): ?>
< li >
< a href = "<?= htmlspecialchars($item['url']) ?>" >
<?= htmlspecialchars($item['title']) ?>
< / a >
< / li >
<?php endforeach; ?>
< / ul >
< / nav >
< / header >
< main >
<?= $content ?>
< / main >
< footer >
< p > © <?= date('Y') ?> My Site< / p >
< / footer >
2026-02-06 18:47:23 +01:00
<?php if (!empty($pageJsUrl)): ?>
< script defer src = "<?= htmlspecialchars($pageJsUrl) ?>?v=<?= $pageJsHash ?? '' ?>" > < / script >
<?php endif; ?>
2025-11-27 23:01:02 +01:00
< / body >
< / html >
```
2026-02-07 19:14:13 +01:00
Key points:
- `$content` contains the rendered output from the page or list template
- Always escape user-facing values with `htmlspecialchars()`
- Check optional variables with `isset()` before using them
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
## Page Template
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
The page template wraps individual page content:
2025-11-27 23:01:02 +01:00
```php
< article >
<?php if (isset($metadata['title'])): ?>
< header >
< h1 > <?= htmlspecialchars($metadata['title']) ?> < / h1 >
2026-02-07 19:14:13 +01:00
2025-11-27 23:01:02 +01:00
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
< time datetime = "<?= $metadata['date'] ?>" >
<?= $metadata['formatted_date'] ?>
< / time >
<?php endif; ?>
< / header >
<?php endif; ?>
< div class = "content" >
<?= $content ?>
< / div >
< / article >
```
2026-02-07 19:14:13 +01:00
## List Template
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
List templates display items from subdirectories:
2025-11-27 23:01:02 +01:00
```php
<?php if ($pageContent): ?>
< div class = "page-intro" >
<?= $pageContent ?>
< / div >
<?php endif; ?>
< div class = "blog-list" >
<?php foreach ($items as $item): ?>
2026-02-07 19:14:13 +01:00
< article >
2025-11-27 23:01:02 +01:00
<?php if (isset($item['cover_image'])): ?>
< a href = "<?= $item['url'] ?>" >
2026-02-07 19:14:13 +01:00
< img src = "<?= $item['cover_image'] ?>"
alt="<?= htmlspecialchars($item['title']) ?> "
loading="lazy">
2025-11-27 23:01:02 +01:00
< / a >
<?php endif; ?>
2026-02-07 19:14:13 +01:00
< h2 >
< a href = "<?= $item['url'] ?>" >
<?= htmlspecialchars($item['title']) ?>
< / a >
< / h2 >
<?php if (isset($item['date'])): ?>
< time datetime = "<?= $item['date'] ?>" >
<?= $item['formatted_date'] ?? $item['date'] ?>
< / time >
<?php endif; ?>
2025-11-27 23:01:02 +01:00
<?php if (isset($item['summary'])): ?>
< p > <?= htmlspecialchars($item['summary']) ?> < / p >
<?php endif; ?>
< / article >
<?php endforeach; ?>
< / div >
```
2026-02-07 19:14:13 +01:00
## Choosing a List Template
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
Select which list template to use per directory in `metadata.ini` :
2025-11-27 23:01:02 +01:00
```ini
title = "Projects"
[settings]
page_template = "list-grid"
```
2026-02-07 19:14:13 +01:00
Built-in options:
- `list` — vertical list (default)
- `list-grid` — card grid
- `list-compact` — minimal compact list
2025-11-27 23:01:02 +01:00
## Creating Custom List Templates
2026-02-07 19:14:13 +01:00
Create a new file in `custom/templates/` . For example, a timeline layout:
2025-11-27 23:01:02 +01:00
**custom/templates/list-timeline.php:**
```php
<?= $pageContent ?>
< div class = "timeline" >
2026-02-07 19:14:13 +01:00
< ?php
2025-11-27 23:01:02 +01:00
$currentYear = null;
2026-02-07 19:14:13 +01:00
foreach ($items as $item):
2025-11-27 23:01:02 +01:00
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
if ($year & & $year !== $currentYear):
$currentYear = $year;
?>
< div class = "year-marker" >
< h3 > <?= $year ?> < / h3 >
< / div >
<?php endif; ?>
2026-02-07 19:14:13 +01:00
2025-11-27 23:01:02 +01:00
< article class = "timeline-item" >
< time > <?= $item['formatted_date'] ?? '' ?> < / time >
2026-02-07 19:14:13 +01:00
< h4 > < a href = "<?= $item['url'] ?>" > <?= htmlspecialchars($item['title']) ?> < / a > < / h4 >
<?php if (isset($item['summary'])): ?>
< p > <?= htmlspecialchars($item['summary']) ?> < / p >
<?php endif; ?>
2025-11-27 23:01:02 +01:00
< / article >
<?php endforeach; ?>
< / div >
```
2026-02-07 19:14:13 +01:00
Use it with:
2025-11-27 23:01:02 +01:00
```ini
[settings]
page_template = "list-timeline"
```
2026-02-07 19:14:13 +01:00
## Template Variables
### Base template
| Variable | Type | Description |
|---|---|---|
| `$content` | string | Rendered page/list HTML |
| `$pageTitle` | string | Page title for `<title>` |
| `$metaDescription` | string | SEO description |
| `$navigation` | array | Menu items (`title` , `url` , `order` ) |
| `$homeLabel` | string | Translated "Home" text |
| `$currentLang` | string | Current language code |
| `$langPrefix` | string | URL language prefix (`""` or `"/no"` ) |
| `$languageUrls` | array | Links to other language versions |
| `$translations` | array | Translated UI strings |
| `$cssHash` | string | Cache-busting hash for base CSS |
| `$pageCssUrl` | string | Page-specific CSS URL (if exists) |
| `$pageJsUrl` | string | Page-specific JS URL (if exists) |
| `$pageLoadTime` | float | Page generation time |
### Page template
| Variable | Type | Description |
|---|---|---|
| `$content` | string | Rendered HTML from content files |
| `$metadata` | array | Page metadata (title, date, etc.) |
### List template
| Variable | Type | Description |
|---|---|---|
| `$items` | array | Items to display |
| `$pageContent` | string | Rendered intro content |
| `$metadata` | array | Directory metadata |
Each item in `$items` :
| Key | Type | Description |
|---|---|---|
| `url` | string | Full URL to item |
| `title` | string | Item title |
| `summary` | string | Short description |
| `date` | string | ISO date (YYYY-MM-DD) |
| `formatted_date` | string | Localized date string |
| `cover_image` | string | Cover image URL |
See the [Template Variables Reference ](../03-reference/03-template-variables.md ) for the complete list.
2025-11-27 23:01:02 +01:00
## Best Practices
2026-02-07 19:14:13 +01:00
**Escape output.** Always use `htmlspecialchars()` for user-facing values. The `$content` variable is already rendered HTML and does not need escaping.
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
**Check variables.** Use `isset()` before accessing optional values:
2025-11-27 23:01:02 +01:00
```php
<?php if (isset($metadata['summary'])): ?>
< p > <?= htmlspecialchars($metadata['summary']) ?> < / p >
<?php endif; ?>
```
2026-02-07 19:14:13 +01:00
**Keep logic minimal.** Templates should display data, not process it. Complex logic belongs in plugins.
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
**Use semantic HTML.** Prefer `<article>` , `<header>` , `<nav>` , `<time>` over generic `<div>` elements.
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
## Next Steps
2025-11-27 23:01:02 +01:00
2026-02-07 19:14:13 +01:00
- [Template Variables Reference ](../03-reference/03-template-variables.md ) — Complete variable list
- [Internationalization ](../03-reference/04-internationalization.md ) — Multi-language support
- [Configuration Reference ](../03-reference/01-configuration.md ) — All configuration options