14 KiB
version, date, author, title, description, summary, quick_tips
version | date | author | title | description | summary | quick_tips | |||
---|---|---|---|---|---|---|---|---|---|
1.0 | 2025-01-15 | DWS Foldsite Team | Template System | Master Foldsite's powerful cascading template system | Learn how Foldsite's hierarchical template discovery works - from simple defaults to sophisticated, context-aware layouts. |
|
Template System
Foldsite's template system is both powerful and intuitive. Templates are HTML files with Jinja2 syntax that define how your content is displayed.
The Template Hierarchy
Foldsite uses a cascading template discovery system. When rendering a page, it searches for templates from most specific to most general, using the first match found.
###Understanding Template Priority
Think of it like CSS specificity - more specific selectors override general ones:
Specific file > Type + Extension > Type + Category > Generic type
Example: Rendering content/blog/my-post.md
Foldsite searches in this order:
templates/blog/my-post.html
← Exact file matchtemplates/blog/__file.md.html
← Markdown files in blog/templates/blog/__file.document.html
← Document files in blog/templates/__file.md.html
← All markdown filestemplates/__file.document.html
← All document filestemplates/__file.html
← Any file- First match wins!
Required Template: base.html
Every Foldsite project MUST have a base.html
in the templates root. This wraps every page on your site.
Minimal base.html
<!DOCTYPE html>
<html>
<head>
<title>My Site</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
{{ content|safe }}
</body>
</html>
Available Variables in base.html
{{ content|safe }}
- Rendered page content (required){{ styles }}
- List of CSS files to load{{ currentPath }}
- Current page path{{ metadata }}
- Frontmatter from markdown files- All template helper functions
Complete base.html Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if metadata and metadata.title %}{{ metadata.title }} - {% endif %}My Site</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
{% for item in get_navigation_items() %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
</nav>
</header>
<main>
{{ content|safe }}
</main>
<footer>
<p>© 2025 My Site</p>
</footer>
</body>
</html>
Template Naming Patterns
File Templates
Templates for individual files use this pattern:
Pattern: __file.{extension}.html
or __file.{category}.html
Examples:
__file.md.html
- All markdown files__file.document.html
- All document types (md, txt, html)__file.image.html
- Individual image pages (rare)__file.jpg.html
- Specific to JPG files
Folder Templates
Templates for directory views:
Pattern: __folder.{category}.html
Examples:
__folder.md.html
- Folders containing mostly markdown__folder.image.html
- Photo gallery folders__folder.html
- Generic folder view
Specific Page Templates
Override for specific pages:
Pattern: {filename}.html
Examples:
index.html
- Only for index.mdabout.html
- Only for about.mdcontact.html
- Only for contact.md
File Categories
Foldsite automatically categorizes files by extension:
Category | Extensions | Template | Use Case |
---|---|---|---|
document | .md , .txt |
__file.document.html |
Text content |
image | .jpg , .png , .gif |
__file.image.html |
Photos |
multimedia | .mp4 , .mp3 |
__file.multimedia.html |
Video/audio |
other | Everything else | __file.other.html |
Downloads |
Files can have multiple categories. For example, .md
files are both md
and document
.
Template Variables
Every template receives these variables:
Always Available
content
- Rendered HTML contentstyles
- List of CSS file pathscurrentPath
- Path relative to content rootmetadata
- Frontmatter dict (for markdown files)
Markdown Files Only
For .md
files, metadata
contains frontmatter:
---
title: "My Blog Post"
date: "2025-01-15"
author: "Your Name"
tags: ["python", "web"]
---
# Content here...
Access in templates:
<h1>{{ metadata.title }}</h1>
<time>{{ metadata.date }}</time>
<p>By {{ metadata.author }}</p>
{% for tag in metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
Template Helpers
Foldsite provides powerful helper functions accessible in all templates:
Content Discovery
{# Get folder contents #}
{% for file in get_folder_contents(currentPath) %}
<a href="/{{ file.path }}">{{ file.name }}</a>
{% endfor %}
{# Get sibling files #}
{% for sibling in get_sibling_content_files(currentPath) %}
<a href="/{{ sibling[1] }}">{{ sibling[0] }}</a>
{% endfor %}
{# Get sibling folders #}
{% for folder in get_sibling_content_folders(currentPath) %}
<a href="/{{ folder[1] }}">{{ folder[0] }}</a>
{% endfor %}
Blog Functions
{# Recent blog posts #}
{% for post in get_recent_posts(limit=5, folder='blog') %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.date }}</time>
</article>
{% endfor %}
{# Posts by tag #}
{% for post in get_posts_by_tag('python', limit=10) %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{# All tags #}
{% for tag in get_all_tags() %}
<a href="/tags/{{ tag.name }}">{{ tag.name }} ({{ tag.count }})</a>
{% endfor %}
Navigation
{# Breadcrumbs #}
{% for crumb in generate_breadcrumbs(currentPath) %}
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% else %}
<span>{{ crumb.title }}</span>
{% endif %}
{% if not loop.last %} / {% endif %}
{% endfor %}
{# Navigation menu #}
{% for item in get_navigation_items(max_items=10) %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
{# Related posts #}
{% for post in get_related_posts(currentPath, limit=3) %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
See Template Helpers Reference for complete documentation.
Cascading Through Directories
Templates cascade down through your directory structure. Place templates in subdirectories to override for specific sections.
Example Structure
templates/
├── base.html # Site-wide wrapper
├── __file.md.html # Default for all markdown
├── __folder.image.html # Default for galleries
├── blog/
│ ├── __file.md.html # Override for blog posts
│ └── __folder.md.html # Override for blog index
└── photos/
└── __folder.image.html # Override for photo galleries
How It Works
Rendering content/blog/my-post.md
:
- Looks in
templates/blog/
first - Finds
blog/__file.md.html
← Uses this - Never checks root
__file.md.html
Rendering content/projects/project.md
:
- Looks in
templates/projects/
first - Doesn't find specific template
- Falls back to
templates/__file.md.html
← Uses this
Practical Examples
Simple Blog Post Template
templates/__file.md.html
:
<article>
{% if metadata %}
<header>
<h1>{{ metadata.title }}</h1>
<time datetime="{{ metadata.date }}">{{ metadata.date }}</time>
{% if metadata.author %}
<p class="author">By {{ metadata.author }}</p>
{% endif %}
</header>
{% endif %}
<div class="content">
{{ content|safe }}
</div>
{% if metadata and metadata.tags %}
<footer>
<p>Tags:
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}">#{{ tag }}</a>
{% endfor %}
</p>
</footer>
{% endif %}
</article>
Blog Index Template
templates/__folder.md.html
:
<div class="blog-index">
<h1>Recent Posts</h1>
{% for post in get_recent_posts(limit=10, folder=currentPath) %}
<article class="post-preview">
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.date }}</time>
{% if post.metadata.description %}
<p>{{ post.metadata.description }}</p>
{% endif %}
</article>
{% endfor %}
</div>
Photo Gallery Template
templates/__folder.image.html
:
<div class="gallery">
{% set breadcrumbs = currentPath.split('/') %}
<nav class="breadcrumbs">
<a href="/">Home</a>
{% for i in range(breadcrumbs|length) %}
{% if i+1 == breadcrumbs|length %}
/ <span>{{ breadcrumbs[i] }}</span>
{% else %}
/ <a href="/{{ '/'.join(breadcrumbs[:i+1]) }}">{{ breadcrumbs[i] }}</a>
{% endif %}
{% endfor %}
</nav>
<div class="photos">
{% for photo in get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
{% if 'image' in photo.categories %}
<a href="/download/{{ photo.path }}" class="photo">
<img src="/download/{{ photo.path }}?max_width=400"
alt="{{ photo.name }}"
loading="lazy">
</a>
{% endif %}
{% endfor %}
</div>
</div>
Error Pages
Custom error template: templates/__error.html
<div class="error-page">
<h1>Error {{ error_code }}</h1>
<p>{{ error_message }}</p>
<p>{{ error_description }}</p>
<a href="/">Return Home</a>
</div>
Variables available:
error_code
- HTTP status code (404, 500, etc.)error_message
- Short message ("Not Found")error_description
- Detailed description
Jinja2 Syntax Quick Reference
Variables
{{ variable }}
{{ metadata.title }}
{{ post.url }}
Filters
{{ content|safe }} {# Don't escape HTML #}
{{ title|upper }} {# Uppercase #}
{{ date|default('Unknown') }} {# Default value #}
{{ items|length }} {# Count items #}
Conditionals
{% if metadata %}
<h1>{{ metadata.title }}</h1>
{% endif %}
{% if metadata and metadata.title %}
...
{% elif metadata %}
...
{% else %}
...
{% endif %}
Loops
{% for item in items %}
<p>{{ item }}</p>
{% endfor %}
{% for key, value in metadata.items() %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
{# Loop variables #}
{% for item in items %}
{{ loop.index }} {# 1-indexed #}
{{ loop.index0 }} {# 0-indexed #}
{{ loop.first }} {# True on first iteration #}
{{ loop.last }} {# True on last iteration #}
{% endfor %}
Filters and Functions
{% for file in get_folder_contents()|sort(attribute='date') %}
...
{% endfor %}
{% for post in get_recent_posts(limit=5)|reverse %}
...
{% endfor %}
Best Practices
1. Start Simple, Add Complexity as Needed
Begin with basic templates:
templates/
├── base.html
├── __file.md.html
└── __folder.md.html
Add specific overrides only when you need different styling or layout.
2. Keep base.html Minimal
Your base template should handle:
- HTML document structure
- CSS loading
- Site-wide navigation
- Footer
Leave content-specific layouts to page templates.
3. Use Template Helpers
Don't manually read files or iterate directories. Use helpers:
✓ Good:
{% for post in get_recent_posts(limit=5) %}
✗ Bad:
{# Trying to manually list files - won't work #}
4. Leverage the Cascade
Put general templates at the root, specific ones in subdirectories:
templates/
├── __file.md.html # Default for all markdown
└── blog/
└── __file.md.html # Special layout for blog
5. Test with Debug Mode
Enable debug mode in config.toml
to see template discovery:
[server]
debug = true
This shows which templates Foldsite considered and why it chose the one it did.
Common Patterns
Pattern: Site Navigation
In base.html
:
<nav>
<a href="/">Home</a>
{% for item in get_navigation_items() %}
<a href="{{ item.url }}"
{% if currentPath == item.path %}class="active"{% endif %}>
{{ item.title }}
</a>
{% endfor %}
</nav>
Pattern: Sidebar with Recent Posts
<aside>
<h3>Recent Posts</h3>
<ul>
{% for post in get_recent_posts(limit=5) %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
<small>{{ post.date }}</small>
</li>
{% endfor %}
</ul>
</aside>
Pattern: Tag Cloud
<div class="tag-cloud">
{% for tag in get_all_tags() %}
<a href="/tags/{{ tag.name|lower }}"
style="font-size: {{ 0.8 + (tag.count * 0.1) }}em;">
{{ tag.name }}
</a>
{% endfor %}
</div>
Next Steps
- Template Discovery - Deep dive into how templates are found
- Template Helpers Reference - Complete API documentation
- Template Recipes - Ready-to-use template examples
- Styles Guide - Styling your templates
Master the template system, and you can build any type of site with Foldsite!