462 lines
11 KiB
Markdown
462 lines
11 KiB
Markdown
|
|
# 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>
|
||
|
|
</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)
|
||
|
|
$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.
|