579 lines
14 KiB
Markdown
579 lines
14 KiB
Markdown
---
|
|
version: "1.0"
|
|
date: "2025-01-15"
|
|
author: "DWS Foldsite Team"
|
|
title: "Template System"
|
|
description: "Master Foldsite's powerful cascading template system"
|
|
summary: "Learn how Foldsite's hierarchical template discovery works - from simple defaults to sophisticated, context-aware layouts."
|
|
quick_tips:
|
|
- "base.html is required and wraps every page on your site"
|
|
- "Templates cascade from specific to general - Foldsite uses the first match"
|
|
- "Use Jinja2 syntax for dynamic content and logic"
|
|
---
|
|
|
|
# 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:
|
|
|
|
1. `templates/blog/my-post.html` ← Exact file match
|
|
2. `templates/blog/__file.md.html` ← Markdown files in blog/
|
|
3. `templates/blog/__file.document.html` ← Document files in blog/
|
|
4. `templates/__file.md.html` ← All markdown files
|
|
5. `templates/__file.document.html` ← All document files
|
|
6. `templates/__file.html` ← Any file
|
|
7. **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
|
|
|
|
```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
|
|
|
|
```html
|
|
<!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.md
|
|
- `about.html` - Only for about.md
|
|
- `contact.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 content
|
|
- `styles` - List of CSS file paths
|
|
- `currentPath` - Path relative to content root
|
|
- `metadata` - Frontmatter dict (for markdown files)
|
|
|
|
### Markdown Files Only
|
|
|
|
For `.md` files, `metadata` contains frontmatter:
|
|
|
|
```markdown
|
|
---
|
|
title: "My Blog Post"
|
|
date: "2025-01-15"
|
|
author: "Your Name"
|
|
tags: ["python", "web"]
|
|
---
|
|
|
|
# Content here...
|
|
```
|
|
|
|
Access in templates:
|
|
|
|
```jinja
|
|
<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
|
|
|
|
```jinja
|
|
{# 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
|
|
|
|
```jinja
|
|
{# 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
|
|
|
|
```jinja
|
|
{# 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](template-helpers.md) 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`:**
|
|
1. Looks in `templates/blog/` first
|
|
2. Finds `blog/__file.md.html` ← **Uses this**
|
|
3. Never checks root `__file.md.html`
|
|
|
|
**Rendering `content/projects/project.md`:**
|
|
1. Looks in `templates/projects/` first
|
|
2. Doesn't find specific template
|
|
3. Falls back to `templates/__file.md.html` ← **Uses this**
|
|
|
|
## Practical Examples
|
|
|
|
### Simple Blog Post Template
|
|
|
|
`templates/__file.md.html`:
|
|
|
|
```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`:
|
|
|
|
```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`:
|
|
|
|
```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`
|
|
|
|
```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
|
|
|
|
```jinja
|
|
{{ variable }}
|
|
{{ metadata.title }}
|
|
{{ post.url }}
|
|
```
|
|
|
|
### Filters
|
|
|
|
```jinja
|
|
{{ content|safe }} {# Don't escape HTML #}
|
|
{{ title|upper }} {# Uppercase #}
|
|
{{ date|default('Unknown') }} {# Default value #}
|
|
{{ items|length }} {# Count items #}
|
|
```
|
|
|
|
### Conditionals
|
|
|
|
```jinja
|
|
{% if metadata %}
|
|
<h1>{{ metadata.title }}</h1>
|
|
{% endif %}
|
|
|
|
{% if metadata and metadata.title %}
|
|
...
|
|
{% elif metadata %}
|
|
...
|
|
{% else %}
|
|
...
|
|
{% endif %}
|
|
```
|
|
|
|
### Loops
|
|
|
|
```jinja
|
|
{% 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
|
|
|
|
```jinja
|
|
{% 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:
|
|
|
|
```jinja
|
|
✓ 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:
|
|
|
|
```toml
|
|
[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`:
|
|
|
|
```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
|
|
|
|
```html
|
|
<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
|
|
|
|
```html
|
|
<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](templates/template-discovery.md)** - Deep dive into how templates are found
|
|
- **[Template Helpers Reference](templates/template-helpers.md)** - Complete API documentation
|
|
- **[Template Recipes](../recipes/)** - Ready-to-use template examples
|
|
- **[Styles Guide](../styles/)** - Styling your templates
|
|
|
|
Master the template system, and you can build any type of site with Foldsite!
|