2025-11-27 23:01:02 +01:00
# Working with Templates
Templates control how your content is presented. FolderWeb uses a simple PHP-based template system—no complex templating languages, just HTML with a sprinkle of PHP.
## Template Types
FolderWeb has three template levels:
### 1. Base Template (`base.php`)
The HTML scaffold wrapping every page:
```
<!DOCTYPE html>
< html lang = "en" >
< head >
< title > Page Title< / title >
< link rel = "stylesheet" href = "..." >
< / head >
< body >
< header > <!-- Navigation --> < / header >
< main > <!-- Page content here --> < / main >
< footer > <!-- Footer --> < / footer >
< / body >
< / html >
```
**You typically customize this once** to set up your site structure.
### 2. Page Template (`page.php`)
Wraps single-page content:
```php
< article >
<?= $content ?>
< / article >
```
**Customize this** to control how individual pages look.
### 3. List Template (`list.php`, `list-grid.php`, `list-compact.php`)
Displays multiple items from subdirectories:
```php
<?= $pageContent ?>
< div class = "item-list" >
<?php foreach ($items as $item): ?>
< article >
< h2 > < a href = "<?= $item['url'] ?>" > <?= $item['title'] ?> < / a > < / h2 >
< p > <?= $item['summary'] ?> < / p >
< / article >
<?php endforeach; ?>
< / div >
```
**Customize this** to control how lists of content (blogs, portfolios, etc.) appear.
## Template Location
Templates live in your `custom/` directory:
```
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
```
**FolderWeb falls back** to `app/default/templates/` if a custom template doesn't exist.
## Customizing the Base Template
Let's modify `base.php` to add your site name and custom navigation:
**custom/templates/base.php:**
```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 >
<?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 ?? '' ?>" >
<?php if (isset($pageCssUrl)): ?>
< link rel = "stylesheet" href = "<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>" >
<?php endif; ?>
< / head >
< body >
< header >
< nav >
< a href = "/" class = "logo" > My Site< / a >
< 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 >
< p >
< small > Generated in <?= number_format($pageLoadTime, 4) ?> s< / small >
< / 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 >
```
**Key points:**
- Always escape user content: `htmlspecialchars($var)`
- Use short echo tags: `<?= $var ?>`
- Check if variables exist: `isset($var)`
- The `$content` variable contains the rendered page/list content
## Customizing Page Templates
The page template wraps your single-page content. Let's add a reading time estimate:
**custom/templates/page.php:**
```php
< article >
<?php if (isset($metadata['title'])): ?>
< header >
< h1 > <?= htmlspecialchars($metadata['title']) ?> < / h1 >
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
< time datetime = "<?= $metadata['date'] ?>" >
<?= $metadata['formatted_date'] ?>
< / time >
<?php endif; ?>
< ?php
// Estimate reading time (avg 200 words/min)
$wordCount = str_word_count(strip_tags($content));
$readingTime = max(1, round($wordCount / 200));
?>
< p class = "reading-time" > <?= $readingTime ?> min read< / p >
< / header >
<?php endif; ?>
< div class = "content" >
<?= $content ?>
< / div >
< / article >
```
## Customizing List Templates
List templates display collections of content. Let's create a custom blog list:
**custom/templates/list.php:**
```php
<?php if ($pageContent): ?>
< div class = "page-intro" >
<?= $pageContent ?>
< / div >
<?php endif; ?>
< div class = "blog-list" >
<?php foreach ($items as $item): ?>
< article class = "blog-item" >
<?php if (isset($item['cover_image'])): ?>
< a href = "<?= $item['url'] ?>" >
< img
src="<?= $item['cover_image'] ?> "
alt="<?= htmlspecialchars($item['title']) ?> "
loading="lazy"
>
< / a >
<?php endif; ?>
< header >
< 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; ?>
< / header >
<?php if (isset($item['summary'])): ?>
< p > <?= htmlspecialchars($item['summary']) ?> < / p >
<?php endif; ?>
< a href = "<?= $item['url'] ?>" > Read more →< / a >
< / article >
<?php endforeach; ?>
< / div >
```
## Choosing List Templates
You can create multiple list templates and select them per directory:
**Available by default:**
- `list.php` — Simple vertical list
- `list-grid.php` — Card grid layout
- `list-compact.php` — Minimal compact list
**Select in metadata.ini:**
```ini
title = "Projects"
[settings]
page_template = "list-grid"
```
Now the `projects/` directory uses the grid layout.
## Creating Custom List Templates
Let's create a timeline template for a blog:
**custom/templates/list-timeline.php:**
```php
<?= $pageContent ?>
< div class = "timeline" >
< ?php
$currentYear = null;
foreach ($items as $item):
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
// Print year marker if it changed
if ($year & & $year !== $currentYear):
$currentYear = $year;
?>
< div class = "year-marker" >
< h3 > <?= $year ?> < / h3 >
< / div >
<?php endif; ?>
< article class = "timeline-item" >
< time > <?= $item['formatted_date'] ?? '' ?> < / time >
< div class = "timeline-content" >
< h4 > < a href = "<?= $item['url'] ?>" > <?= htmlspecialchars($item['title']) ?> < / a > < / h4 >
<?php if (isset($item['summary'])): ?>
< p > <?= htmlspecialchars($item['summary']) ?> < / p >
<?php endif; ?>
< / div >
< / article >
<?php endforeach; ?>
< / div >
```
**Use in metadata.ini:**
```ini
[settings]
page_template = "list-timeline"
```
## Available Template Variables
Templates have access to these variables (see [Reference: Template Variables ](# ) for complete list):
**Base template:**
```php
$content // Rendered page/list HTML
$pageTitle // Page title for < title > tag
$metaDescription // SEO description
$navigation // Array of menu items
$homeLabel // "Home" link text (translated)
$currentLang // Current language code
$languageUrls // Links to other language versions
$translations // Translated UI strings
$cssHash // Cache-busting hash for CSS
$pageCssUrl // Page-specific CSS URL (if exists)
2026-02-06 18:47:23 +01:00
$pageJsUrl // Page-specific JS URL (if exists)
2025-11-27 23:01:02 +01:00
$pageLoadTime // Page generation time
```
**Page template:**
```php
$content // Rendered HTML
$metadata // Metadata array (title, date, etc.)
```
**List template:**
```php
$items // Array of items to display
$pageContent // Optional intro content from page
$metadata // Directory metadata
// Each $item has:
$item['url'] // Full URL to item
$item['title'] // Item title
$item['summary'] // Short description
$item['date'] // ISO date (YYYY-MM-DD)
$item['formatted_date'] // Localized date string
$item['cover_image'] // Cover image URL (if exists)
```
## Best Practices
### 1. Always Escape Output
```php
<!-- Bad -->
< h1 > <?= $title ?> < / h1 >
<!-- Good -->
< h1 > <?= htmlspecialchars($title) ?> < / h1 >
```
**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown).
### 2. Check Variables Exist
```php
<!-- Bad -->
< p > <?= $metadata['summary'] ?> < / p >
<!-- Good -->
<?php if (isset($metadata['summary'])): ?>
< p > <?= htmlspecialchars($metadata['summary']) ?> < / p >
<?php endif; ?>
```
### 3. Use Short Echo Tags
```php
<!-- Verbose -->
<?php echo htmlspecialchars($title); ?>
<!-- Concise -->
<?= htmlspecialchars($title) ?>
```
### 4. Keep Logic Minimal
Templates should display data, not process it. Complex logic belongs in plugins.
```php
<!-- Bad: complex logic in template -->
< ?php
$posts = array_filter($items, function($item) {
return strtotime($item['date']) > strtotime('-30 days');
});
usort($posts, function($a, $b) {
return strcmp($b['date'], $a['date']);
});
?>
<!-- Good: prepare data in a plugin, display in template -->
<?php foreach ($recentPosts as $post): ?>
...
<?php endforeach; ?>
```
### 5. Use Semantic HTML
```php
<!-- Bad -->
< div class = "title" > Title< / div >
< div class = "content" > Content< / div >
<!-- Good -->
< article >
< h1 > Title< / h1 >
< div class = "content" > Content< / div >
< / article >
```
## Practical Examples
### Simple Portfolio Page Template
```php
< article class = "portfolio-item" >
< header >
<?php if (isset($metadata['cover_image'])): ?>
< img src = "<?= $metadata['cover_image'] ?>" alt = "" >
<?php endif; ?>
< h1 > <?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?> < / h1 >
< / header >
< div class = "content" >
<?= $content ?>
< / div >
<?php if (isset($metadata['project_url'])): ?>
< footer >
< a href = "<?= htmlspecialchars($metadata['project_url']) ?>"
class="button">View Project →< / a >
< / footer >
<?php endif; ?>
< / article >
```
### Card Grid List Template
```php
<?= $pageContent ?>
< div class = "card-grid" >
<?php foreach ($items as $item): ?>
< article class = "card" >
<?php if (isset($item['cover_image'])): ?>
< img src = "<?= $item['cover_image'] ?>" alt = "" loading = "lazy" >
<?php endif; ?>
< div class = "card-content" >
< h3 >
< a href = "<?= $item['url'] ?>" >
<?= htmlspecialchars($item['title']) ?>
< / a >
< / h3 >
<?php if (isset($item['summary'])): ?>
< p > <?= htmlspecialchars($item['summary']) ?> < / p >
<?php endif; ?>
< / div >
< / article >
<?php endforeach; ?>
< / div >
```
## What's Next?
You now know how to customize templates. Next, learn about:
- **[Template Variables Reference ](# )** — Complete list of available variables
- **[Creating Plugins ](# )** — Extend functionality and add custom data to templates
- **[Internationalization ](# )** — Build multilingual sites
Or explore the examples in `app/default/content/examples/templates-demo/` to see templates in action.