folderweb/docs/02-tutorial/03-templates.md
Ruben f1447049e4 Add page-specific JavaScript support
Update documentation for new JS variables and rendering pipeline

Add script.js file support to content directories

Implement cache-busting for JavaScript files

Update static file serving to include JavaScript files

Document page-specific script loading in base.php
2026-02-06 18:47:23 +01:00

11 KiB

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:

<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:

<?= $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:

<!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>&copy; <?= date('Y') ?> My Site</p>
    <p>
      <small>Generated in <?= number_format($pageLoadTime, 4) ?>s</small>
    </p>
  </footer>

  <?php if (!empty($pageJsUrl)): ?>
    <script defer src="<?= htmlspecialchars($pageJsUrl) ?>?v=<?= $pageJsHash ?? '' ?>"></script>
  <?php endif; ?>
</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:

<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 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:

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:

<?= $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:

[settings]
page_template = "list-timeline"

Available Template Variables

Templates have access to these variables (see Reference: Template Variables for complete list):

Base template:

$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)
$pageJsUrl        // Page-specific JS URL (if exists)
$pageLoadTime     // Page generation time

Page template:

$content          // Rendered HTML
$metadata         // Metadata array (title, date, etc.)

List template:

$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

<!-- Bad -->
<h1><?= $title ?></h1>

<!-- Good -->
<h1><?= htmlspecialchars($title) ?></h1>

Exception: Already-sanitized HTML like $content (rendered from Markdown).

2. Check Variables Exist

<!-- Bad -->
<p><?= $metadata['summary'] ?></p>

<!-- Good -->
<?php if (isset($metadata['summary'])): ?>
  <p><?= htmlspecialchars($metadata['summary']) ?></p>
<?php endif; ?>

3. Use Short Echo Tags

<!-- Verbose -->
<?php echo htmlspecialchars($title); ?>

<!-- Concise -->
<?= htmlspecialchars($title) ?>

4. Keep Logic Minimal

Templates should display data, not process it. Complex logic belongs in plugins.

<!-- 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

<!-- 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

<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

<?= $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:

Or explore the examples in app/default/content/examples/templates-demo/ to see templates in action.