720 lines
17 KiB
Markdown
720 lines
17 KiB
Markdown
|
|
# Creating Custom Templates
|
||
|
|
|
||
|
|
Templates control the HTML structure and presentation of your content. This guide covers advanced template creation, from simple page layouts to complex list views.
|
||
|
|
|
||
|
|
## Template Hierarchy
|
||
|
|
|
||
|
|
FolderWeb uses a three-level template system:
|
||
|
|
|
||
|
|
1. **Base template** (`base.php`) — The HTML scaffold wrapping everything
|
||
|
|
2. **Content template** — Either `page.php` or a list template
|
||
|
|
3. **Partials** (optional) — Reusable components you create
|
||
|
|
|
||
|
|
```
|
||
|
|
base.php
|
||
|
|
└── page.php or list.php
|
||
|
|
└── Rendered content
|
||
|
|
```
|
||
|
|
|
||
|
|
## Template Resolution
|
||
|
|
|
||
|
|
When rendering a page, FolderWeb looks for templates in this order:
|
||
|
|
|
||
|
|
**For page views:**
|
||
|
|
1. `custom/templates/page.php`
|
||
|
|
2. `app/default/templates/page.php` (fallback)
|
||
|
|
|
||
|
|
**For list views:**
|
||
|
|
1. `custom/templates/{page_template}.php` (e.g., `list-grid.php`)
|
||
|
|
2. `custom/templates/list.php`
|
||
|
|
3. `app/default/templates/{page_template}.php`
|
||
|
|
4. `app/default/templates/list.php` (fallback)
|
||
|
|
|
||
|
|
**For base:**
|
||
|
|
1. `custom/templates/base.php`
|
||
|
|
2. `app/default/templates/base.php` (fallback)
|
||
|
|
|
||
|
|
## Creating a Custom Base Template
|
||
|
|
|
||
|
|
The base template defines the HTML structure for every page.
|
||
|
|
|
||
|
|
### Step 1: Copy the Default
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cp app/default/templates/base.php custom/templates/base.php
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 2: Customize
|
||
|
|
|
||
|
|
**custom/templates/base.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title><?= htmlspecialchars($pageTitle ?? 'My Site') ?></title>
|
||
|
|
|
||
|
|
<?php if (!empty($metaDescription)): ?>
|
||
|
|
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<!-- Open Graph -->
|
||
|
|
<meta property="og:title" content="<?= htmlspecialchars($pageTitle ?? 'My Site') ?>">
|
||
|
|
<?php if (!empty($metaDescription)): ?>
|
||
|
|
<meta property="og:description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||
|
|
<?php endif; ?>
|
||
|
|
<?php if (!empty($socialImageUrl)): ?>
|
||
|
|
<meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<!-- Styles -->
|
||
|
|
<link rel="stylesheet" href="/custom/styles/base.css">
|
||
|
|
<?php if (!empty($pageCssUrl)): ?>
|
||
|
|
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
||
|
|
<?php endif; ?>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<a href="#main" class="skip-link">Skip to main content</a>
|
||
|
|
|
||
|
|
<header class="site-header">
|
||
|
|
<div class="container">
|
||
|
|
<a href="<?= htmlspecialchars($langPrefix ?? '') ?>/" class="site-title">
|
||
|
|
My Website
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<nav class="main-nav" aria-label="Main navigation">
|
||
|
|
<ul>
|
||
|
|
<?php foreach ($navigation ?? [] as $item): ?>
|
||
|
|
<li>
|
||
|
|
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||
|
|
<?= htmlspecialchars($item['title']) ?>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</ul>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
|
||
|
|
<nav class="language-switcher" aria-label="Language">
|
||
|
|
<?php foreach ($languageUrls as $lang => $url): ?>
|
||
|
|
<a href="<?= htmlspecialchars($url) ?>"
|
||
|
|
<?= ($lang === ($currentLang ?? 'en')) ? 'aria-current="true"' : '' ?>>
|
||
|
|
<?= htmlspecialchars(strtoupper($lang)) ?>
|
||
|
|
</a>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</nav>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<main id="main" class="site-main">
|
||
|
|
<div class="container">
|
||
|
|
<?= $content ?>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<footer class="site-footer">
|
||
|
|
<div class="container">
|
||
|
|
<nav aria-label="Footer navigation">
|
||
|
|
<a href="/privacy/">Privacy</a>
|
||
|
|
<a href="/terms/">Terms</a>
|
||
|
|
<a href="/contact/">Contact</a>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<p class="copyright">
|
||
|
|
© <?= date('Y') ?> My Website
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<p class="performance">
|
||
|
|
<?= htmlspecialchars($translations['footer_handcoded'] ?? 'Generated in') ?>
|
||
|
|
<?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?>
|
||
|
|
<?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</footer>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Key Features
|
||
|
|
|
||
|
|
- **Skip link** for accessibility
|
||
|
|
- **Container divs** for layout control
|
||
|
|
- **Semantic HTML** (header, nav, main, footer)
|
||
|
|
- **ARIA labels** for screen readers
|
||
|
|
- **Open Graph tags** for social media
|
||
|
|
- **Performance metrics** in footer
|
||
|
|
|
||
|
|
## Creating Custom Page Templates
|
||
|
|
|
||
|
|
Page templates wrap single-page content.
|
||
|
|
|
||
|
|
### Blog Post Template
|
||
|
|
|
||
|
|
**custom/templates/page.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<article class="blog-post">
|
||
|
|
<header class="post-header">
|
||
|
|
<?php if (isset($metadata['title'])): ?>
|
||
|
|
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div class="post-meta">
|
||
|
|
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||
|
|
<time datetime="<?= $metadata['date'] ?>">
|
||
|
|
<?= $metadata['formatted_date'] ?? $metadata['date'] ?>
|
||
|
|
</time>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (isset($metadata['author'])): ?>
|
||
|
|
<span class="author">
|
||
|
|
by <?= htmlspecialchars($metadata['author']) ?>
|
||
|
|
</span>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (isset($readingTime)): ?>
|
||
|
|
<span class="reading-time">
|
||
|
|
<?= $readingTime ?> min read
|
||
|
|
</span>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if (isset($metadata['tags'])): ?>
|
||
|
|
<div class="post-tags">
|
||
|
|
<?php foreach (explode(',', $metadata['tags']) as $tag): ?>
|
||
|
|
<span class="tag"><?= htmlspecialchars(trim($tag)) ?></span>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="post-content">
|
||
|
|
<?= $content ?>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if (!empty($relatedPosts)): ?>
|
||
|
|
<aside class="related-posts">
|
||
|
|
<h2>Related Posts</h2>
|
||
|
|
<ul>
|
||
|
|
<?php foreach ($relatedPosts as $post): ?>
|
||
|
|
<li>
|
||
|
|
<a href="<?= htmlspecialchars($post['url']) ?>">
|
||
|
|
<?= htmlspecialchars($post['title']) ?>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</ul>
|
||
|
|
</aside>
|
||
|
|
<?php endif; ?>
|
||
|
|
</article>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Portfolio Item Template
|
||
|
|
|
||
|
|
**custom/templates/page-portfolio.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<article class="portfolio-item">
|
||
|
|
<?php if (isset($metadata['cover_image'])): ?>
|
||
|
|
<div class="project-hero">
|
||
|
|
<img src="<?= $metadata['cover_image'] ?>"
|
||
|
|
alt="<?= htmlspecialchars($metadata['title'] ?? '') ?>">
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<header class="project-header">
|
||
|
|
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||
|
|
|
||
|
|
<dl class="project-details">
|
||
|
|
<?php if (isset($metadata['client'])): ?>
|
||
|
|
<dt>Client</dt>
|
||
|
|
<dd><?= htmlspecialchars($metadata['client']) ?></dd>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (isset($metadata['year'])): ?>
|
||
|
|
<dt>Year</dt>
|
||
|
|
<dd><?= htmlspecialchars($metadata['year']) ?></dd>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (isset($metadata['role'])): ?>
|
||
|
|
<dt>Role</dt>
|
||
|
|
<dd><?= htmlspecialchars($metadata['role']) ?></dd>
|
||
|
|
<?php endif; ?>
|
||
|
|
</dl>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="project-content">
|
||
|
|
<?= $content ?>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if (isset($metadata['project_url'])): ?>
|
||
|
|
<footer class="project-footer">
|
||
|
|
<a href="<?= htmlspecialchars($metadata['project_url']) ?>"
|
||
|
|
class="button" target="_blank" rel="noopener">
|
||
|
|
View Live Project →
|
||
|
|
</a>
|
||
|
|
</footer>
|
||
|
|
<?php endif; ?>
|
||
|
|
</article>
|
||
|
|
```
|
||
|
|
|
||
|
|
**To use:** Set in metadata:
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[settings]
|
||
|
|
page_template = "page-portfolio"
|
||
|
|
```
|
||
|
|
|
||
|
|
Wait, that won't work for page templates—only list templates use `page_template`. For page templates, you'd need to select via a plugin or use different template files per directory. Let's stick with one `page.php` that adapts based on metadata.
|
||
|
|
|
||
|
|
## Creating Custom List Templates
|
||
|
|
|
||
|
|
List templates display collections of items.
|
||
|
|
|
||
|
|
### Card Grid Layout
|
||
|
|
|
||
|
|
**custom/templates/list-cards.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php if ($pageContent): ?>
|
||
|
|
<div class="list-intro">
|
||
|
|
<?= $pageContent ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div class="card-grid">
|
||
|
|
<?php foreach ($items as $item): ?>
|
||
|
|
<article class="card">
|
||
|
|
<?php if (isset($item['cover_image'])): ?>
|
||
|
|
<a href="<?= $item['url'] ?>" class="card-image">
|
||
|
|
<img src="<?= $item['cover_image'] ?>"
|
||
|
|
alt="<?= htmlspecialchars($item['title']) ?>"
|
||
|
|
loading="lazy">
|
||
|
|
</a>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div class="card-content">
|
||
|
|
<h2 class="card-title">
|
||
|
|
<a href="<?= $item['url'] ?>">
|
||
|
|
<?= htmlspecialchars($item['title']) ?>
|
||
|
|
</a>
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<?php if (isset($item['date'])): ?>
|
||
|
|
<time class="card-date" datetime="<?= $item['date'] ?>">
|
||
|
|
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||
|
|
</time>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (isset($item['summary'])): ?>
|
||
|
|
<p class="card-summary">
|
||
|
|
<?= htmlspecialchars($item['summary']) ?>
|
||
|
|
</p>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<a href="<?= $item['url'] ?>" class="card-link">
|
||
|
|
Read more →
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Corresponding CSS:**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.card-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
||
|
|
gap: 2rem;
|
||
|
|
margin-top: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card {
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
overflow: hidden;
|
||
|
|
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
transform: translateY(-4px);
|
||
|
|
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-image img {
|
||
|
|
width: 100%;
|
||
|
|
aspect-ratio: 16 / 9;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-content {
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-title {
|
||
|
|
margin: 0 0 0.5rem;
|
||
|
|
font-size: 1.25rem;
|
||
|
|
|
||
|
|
& a {
|
||
|
|
color: inherit;
|
||
|
|
text-decoration: none;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-date {
|
||
|
|
display: block;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
margin-bottom: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-summary {
|
||
|
|
margin: 0 0 1rem;
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-link {
|
||
|
|
font-weight: 500;
|
||
|
|
text-decoration: none;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Timeline Layout
|
||
|
|
|
||
|
|
**custom/templates/list-timeline.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?= $pageContent ?>
|
||
|
|
|
||
|
|
<div class="timeline">
|
||
|
|
<?php
|
||
|
|
$currentYear = null;
|
||
|
|
foreach ($items as $item):
|
||
|
|
// Extract year from date
|
||
|
|
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
|
||
|
|
|
||
|
|
// Show year marker when year changes
|
||
|
|
if ($year && $year !== $currentYear):
|
||
|
|
$currentYear = $year;
|
||
|
|
?>
|
||
|
|
<div class="timeline-year">
|
||
|
|
<h2><?= $year ?></h2>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<article class="timeline-item">
|
||
|
|
<time class="timeline-date">
|
||
|
|
<?= $item['formatted_date'] ?? ($item['date'] ?? '') ?>
|
||
|
|
</time>
|
||
|
|
|
||
|
|
<div class="timeline-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>
|
||
|
|
```
|
||
|
|
|
||
|
|
**CSS:**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.timeline {
|
||
|
|
position: relative;
|
||
|
|
max-width: 800px;
|
||
|
|
margin: 2rem 0;
|
||
|
|
padding-left: 2rem;
|
||
|
|
|
||
|
|
&::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
left: 0;
|
||
|
|
top: 0;
|
||
|
|
bottom: 0;
|
||
|
|
width: 2px;
|
||
|
|
background: var(--color-border);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-year {
|
||
|
|
margin: 2rem 0 1rem;
|
||
|
|
|
||
|
|
& h2 {
|
||
|
|
font-size: 1.5rem;
|
||
|
|
color: var(--color-accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-item {
|
||
|
|
position: relative;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
|
||
|
|
&::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
left: -2.5rem;
|
||
|
|
top: 0.5rem;
|
||
|
|
width: 0.75rem;
|
||
|
|
height: 0.75rem;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: var(--color-accent);
|
||
|
|
border: 2px solid var(--color-bg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-date {
|
||
|
|
display: block;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-content {
|
||
|
|
& h3 {
|
||
|
|
margin: 0 0 0.5rem;
|
||
|
|
font-size: 1.125rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
& p {
|
||
|
|
margin: 0;
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Magazine Layout
|
||
|
|
|
||
|
|
**custom/templates/list-magazine.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?= $pageContent ?>
|
||
|
|
|
||
|
|
<?php if (!empty($items)): ?>
|
||
|
|
<div class="magazine-layout">
|
||
|
|
<!-- Featured post (first item) -->
|
||
|
|
<?php $featured = array_shift($items); ?>
|
||
|
|
<article class="magazine-featured">
|
||
|
|
<?php if (isset($featured['cover_image'])): ?>
|
||
|
|
<a href="<?= $featured['url'] ?>" class="featured-image">
|
||
|
|
<img src="<?= $featured['cover_image'] ?>"
|
||
|
|
alt="<?= htmlspecialchars($featured['title']) ?>">
|
||
|
|
</a>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div class="featured-content">
|
||
|
|
<h2>
|
||
|
|
<a href="<?= $featured['url'] ?>">
|
||
|
|
<?= htmlspecialchars($featured['title']) ?>
|
||
|
|
</a>
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<?php if (isset($featured['summary'])): ?>
|
||
|
|
<p class="featured-summary">
|
||
|
|
<?= htmlspecialchars($featured['summary']) ?>
|
||
|
|
</p>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<a href="<?= $featured['url'] ?>" class="read-more">
|
||
|
|
Read article →
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- Remaining posts in grid -->
|
||
|
|
<?php if (!empty($items)): ?>
|
||
|
|
<div class="magazine-grid">
|
||
|
|
<?php foreach ($items as $item): ?>
|
||
|
|
<article class="magazine-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; ?>
|
||
|
|
|
||
|
|
<h3>
|
||
|
|
<a href="<?= $item['url'] ?>">
|
||
|
|
<?= htmlspecialchars($item['title']) ?>
|
||
|
|
</a>
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
<?php if (isset($item['date'])): ?>
|
||
|
|
<time datetime="<?= $item['date'] ?>">
|
||
|
|
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||
|
|
</time>
|
||
|
|
<?php endif; ?>
|
||
|
|
</article>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Using Partials (Template Includes)
|
||
|
|
|
||
|
|
Break complex templates into reusable components.
|
||
|
|
|
||
|
|
### Creating a Partial
|
||
|
|
|
||
|
|
**custom/templates/partials/post-card.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<article class="post-card">
|
||
|
|
<?php if (isset($post['cover_image'])): ?>
|
||
|
|
<a href="<?= $post['url'] ?>">
|
||
|
|
<img src="<?= $post['cover_image'] ?>"
|
||
|
|
alt="<?= htmlspecialchars($post['title']) ?>">
|
||
|
|
</a>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<h3>
|
||
|
|
<a href="<?= $post['url'] ?>">
|
||
|
|
<?= htmlspecialchars($post['title']) ?>
|
||
|
|
</a>
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
<?php if (isset($post['summary'])): ?>
|
||
|
|
<p><?= htmlspecialchars($post['summary']) ?></p>
|
||
|
|
<?php endif; ?>
|
||
|
|
</article>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Using a Partial
|
||
|
|
|
||
|
|
**custom/templates/list.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?= $pageContent ?>
|
||
|
|
|
||
|
|
<div class="post-list">
|
||
|
|
<?php foreach ($items as $post): ?>
|
||
|
|
<?php include __DIR__ . '/partials/post-card.php'; ?>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note:** Set `$post` before including, as the partial expects it.
|
||
|
|
|
||
|
|
## Conditional Templates
|
||
|
|
|
||
|
|
Use metadata to vary presentation.
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php if (isset($metadata['layout']) && $metadata['layout'] === 'wide'): ?>
|
||
|
|
<article class="wide-layout">
|
||
|
|
<?= $content ?>
|
||
|
|
</article>
|
||
|
|
<?php else: ?>
|
||
|
|
<article class="standard-layout">
|
||
|
|
<div class="container">
|
||
|
|
<?= $content ?>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
<?php endif; ?>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Set in metadata:**
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[settings]
|
||
|
|
layout = "wide"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Template Best Practices
|
||
|
|
|
||
|
|
### 1. Always Escape Output
|
||
|
|
|
||
|
|
```php
|
||
|
|
<!-- Good -->
|
||
|
|
<h1><?= htmlspecialchars($title) ?></h1>
|
||
|
|
|
||
|
|
<!-- Bad -->
|
||
|
|
<h1><?= $title ?></h1>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Check Before Using
|
||
|
|
|
||
|
|
```php
|
||
|
|
<!-- Good -->
|
||
|
|
<?php if (isset($metadata['author'])): ?>
|
||
|
|
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<!-- Bad -->
|
||
|
|
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Use Semantic HTML
|
||
|
|
|
||
|
|
```php
|
||
|
|
<!-- Good -->
|
||
|
|
<article>
|
||
|
|
<header><h1>Title</h1></header>
|
||
|
|
<div class="content">Content</div>
|
||
|
|
<footer>Meta</footer>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- Bad -->
|
||
|
|
<div class="post">
|
||
|
|
<div class="title">Title</div>
|
||
|
|
<div class="content">Content</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Add ARIA Labels
|
||
|
|
|
||
|
|
```php
|
||
|
|
<nav aria-label="Main navigation">
|
||
|
|
<!-- navigation items -->
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<nav aria-label="Language">
|
||
|
|
<!-- language switcher -->
|
||
|
|
</nav>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Keep Logic Minimal
|
||
|
|
|
||
|
|
```php
|
||
|
|
<!-- Good: simple check -->
|
||
|
|
<?php if (isset($item['date'])): ?>
|
||
|
|
<time><?= $item['formatted_date'] ?></time>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<!-- Bad: complex logic (move to plugin) -->
|
||
|
|
<?php
|
||
|
|
$recentPosts = array_filter($items, fn($item) =>
|
||
|
|
strtotime($item['date']) > strtotime('-30 days')
|
||
|
|
);
|
||
|
|
usort($recentPosts, fn($a, $b) => strcmp($b['date'], $a['date']));
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
## What's Next?
|
||
|
|
|
||
|
|
- **[Template Variables Reference](#)** — See all available variables
|
||
|
|
- **[Plugin System](#)** — Add custom variables to templates
|
||
|
|
- **[Styling Guide](#)** — Style your custom templates
|