docs refactor
All checks were successful
All checks were successful
This commit is contained in:
578
docs/content/templates/index.md
Normal file
578
docs/content/templates/index.md
Normal file
@ -0,0 +1,578 @@
|
||||
---
|
||||
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!
|
558
docs/content/templates/template-discovery.md
Normal file
558
docs/content/templates/template-discovery.md
Normal file
@ -0,0 +1,558 @@
|
||||
---
|
||||
version: "1.0"
|
||||
date: "2025-01-15"
|
||||
author: "DWS Foldsite Team"
|
||||
title: "Template Discovery System"
|
||||
description: "Understanding how Foldsite finds and chooses templates"
|
||||
summary: "Deep dive into Foldsite's hierarchical template discovery algorithm - learn exactly how templates are matched to content."
|
||||
quick_tips:
|
||||
- "Templates are searched from most specific to most general"
|
||||
- "First match wins - more specific templates override general ones"
|
||||
- "Templates cascade down through directory hierarchies"
|
||||
---
|
||||
|
||||
# Template Discovery System
|
||||
|
||||
Understanding how Foldsite discovers and chooses templates is key to building sophisticated sites. This guide explains the complete template resolution algorithm.
|
||||
|
||||
## The Discovery Algorithm
|
||||
|
||||
When Foldsite renders a page, it follows a **hierarchical search pattern** to find the best template.
|
||||
|
||||
### Core Principle
|
||||
|
||||
**Specificity wins.** More specific templates override general ones.
|
||||
|
||||
Think of it like CSS specificity:
|
||||
```
|
||||
#specific-id (most specific)
|
||||
.class-name
|
||||
element (least specific)
|
||||
```
|
||||
|
||||
In Foldsite:
|
||||
```
|
||||
my-post.html (most specific - exact file)
|
||||
__file.md.html (category + extension)
|
||||
__file.document.html (category only)
|
||||
__file.html (least specific - any file)
|
||||
```
|
||||
|
||||
## File Type Detection
|
||||
|
||||
Before template discovery, Foldsite determines the content type:
|
||||
|
||||
### For Files
|
||||
|
||||
**Step 1:** Extract extension
|
||||
```
|
||||
blog/my-post.md → extension: "md"
|
||||
```
|
||||
|
||||
**Step 2:** Map to categories
|
||||
```
|
||||
"md" → categories: ["document", "md"]
|
||||
```
|
||||
|
||||
**Category mapping:**
|
||||
```python
|
||||
GENERIC_FILE_MAPPING = {
|
||||
"document": ["md", "txt", "html"],
|
||||
"image": ["png", "jpg", "jpeg", "gif", "svg"],
|
||||
"multimedia": ["mp4", "mp3", "webm"],
|
||||
"other": [...] # Everything else
|
||||
}
|
||||
```
|
||||
|
||||
### For Folders
|
||||
|
||||
**Step 1:** Count file types in folder
|
||||
```
|
||||
photos/vacation/
|
||||
IMG_001.jpg → jpg: 1
|
||||
IMG_002.jpg → jpg: 2
|
||||
IMG_003.jpg → jpg: 3
|
||||
notes.txt → txt: 1
|
||||
```
|
||||
|
||||
**Step 2:** Most common type wins
|
||||
```
|
||||
Most common: jpg (3 files)
|
||||
→ categories: ["image", "jpg"]
|
||||
```
|
||||
|
||||
## Template Search Order
|
||||
|
||||
### For Files: `blog/my-post.md`
|
||||
|
||||
**Given:**
|
||||
- Path: `blog/my-post.md`
|
||||
- Type: `file`
|
||||
- Categories: `["document", "md"]`
|
||||
- Extension: `md`
|
||||
|
||||
**Search order:**
|
||||
|
||||
1. **Exact file match** in current directory
|
||||
```
|
||||
templates/blog/my-post.html
|
||||
```
|
||||
|
||||
2. **Type + extension** in current directory
|
||||
```
|
||||
templates/blog/__file.md.html
|
||||
```
|
||||
|
||||
3. **Type + category** in current directory (most specific category first)
|
||||
```
|
||||
templates/blog/__file.document.html
|
||||
```
|
||||
|
||||
4. **Generic type** in current directory
|
||||
```
|
||||
templates/blog/__file.html
|
||||
```
|
||||
|
||||
5. **Move up one directory**, repeat steps 2-4
|
||||
```
|
||||
templates/__file.md.html
|
||||
templates/__file.document.html
|
||||
templates/__file.html
|
||||
```
|
||||
|
||||
6. **Default template** (if exists)
|
||||
```
|
||||
templates/default.html
|
||||
```
|
||||
|
||||
**First match wins!**
|
||||
|
||||
### For Folders: `photos/vacation/`
|
||||
|
||||
**Given:**
|
||||
- Path: `photos/vacation/`
|
||||
- Type: `folder`
|
||||
- Categories: `["image"]` (most files are images)
|
||||
|
||||
**Search order:**
|
||||
|
||||
1. **Exact folder match**
|
||||
```
|
||||
templates/photos/vacation/__folder.html
|
||||
```
|
||||
|
||||
2. **Type + category** in current directory
|
||||
```
|
||||
templates/photos/__folder.image.html
|
||||
```
|
||||
|
||||
3. **Generic folder** in current directory
|
||||
```
|
||||
templates/photos/__folder.html
|
||||
```
|
||||
|
||||
4. **Move up one directory**, repeat steps 2-3
|
||||
```
|
||||
templates/__folder.image.html
|
||||
templates/__folder.html
|
||||
```
|
||||
|
||||
5. **Default template**
|
||||
```
|
||||
templates/default.html
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Blog Post
|
||||
|
||||
**Content:** `content/blog/posts/2024-my-post.md`
|
||||
|
||||
**Template search:**
|
||||
```
|
||||
1. templates/blog/posts/2024-my-post.html ✗ Not found
|
||||
2. templates/blog/posts/__file.md.html ✗ Not found
|
||||
3. templates/blog/posts/__file.document.html ✗ Not found
|
||||
4. templates/blog/posts/__file.html ✗ Not found
|
||||
5. templates/blog/__file.md.html ✓ FOUND!
|
||||
```
|
||||
|
||||
**Result:** Uses `templates/blog/__file.md.html`
|
||||
|
||||
**Why:** Most specific template found in the hierarchy.
|
||||
|
||||
### Example 2: Homepage
|
||||
|
||||
**Content:** `content/index.md`
|
||||
|
||||
**Template search:**
|
||||
```
|
||||
1. templates/index.html ✓ FOUND!
|
||||
```
|
||||
|
||||
**Result:** Uses `templates/index.html`
|
||||
|
||||
**Why:** Exact file match (most specific possible).
|
||||
|
||||
### Example 3: Photo Gallery
|
||||
|
||||
**Content:** `content/photos/vacation/` (folder with 50 JPG files)
|
||||
|
||||
**Categories:** `["image"]`
|
||||
|
||||
**Template search:**
|
||||
```
|
||||
1. templates/photos/vacation/__folder.html ✗ Not found
|
||||
2. templates/photos/__folder.image.html ✓ FOUND!
|
||||
```
|
||||
|
||||
**Result:** Uses `templates/photos/__folder.image.html`
|
||||
|
||||
**Why:** Type + category match in parent directory.
|
||||
|
||||
### Example 4: Deep Nesting
|
||||
|
||||
**Content:** `content/docs/guides/advanced/testing.md`
|
||||
|
||||
**Template search:**
|
||||
```
|
||||
1. templates/docs/guides/advanced/testing.html ✗ Not found
|
||||
2. templates/docs/guides/advanced/__file.md.html ✗ Not found
|
||||
3. templates/docs/guides/__file.md.html ✗ Not found
|
||||
4. templates/docs/__file.md.html ✓ FOUND!
|
||||
```
|
||||
|
||||
**Result:** Uses `templates/docs/__file.md.html`
|
||||
|
||||
**Why:** Climbs directory tree until finding a match.
|
||||
|
||||
## Template Cascade
|
||||
|
||||
Templates **cascade down** through directories:
|
||||
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← Default for ALL markdown
|
||||
└── blog/
|
||||
└── __file.md.html ← Override for blog/ only
|
||||
```
|
||||
|
||||
**Rendering `content/about.md`:**
|
||||
- Uses `templates/__file.md.html`
|
||||
|
||||
**Rendering `content/blog/post.md`:**
|
||||
- Uses `templates/blog/__file.md.html` (more specific)
|
||||
|
||||
### Multi-Level Cascade
|
||||
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← Level 1: Site default
|
||||
├── blog/
|
||||
│ └── __file.md.html ← Level 2: Blog default
|
||||
└── blog/
|
||||
└── tutorials/
|
||||
└── __file.md.html ← Level 3: Tutorial-specific
|
||||
```
|
||||
|
||||
**Effect:**
|
||||
- `content/about.md` → Level 1 template
|
||||
- `content/blog/news.md` → Level 2 template
|
||||
- `content/blog/tutorials/python.md` → Level 3 template
|
||||
|
||||
## Category Priority
|
||||
|
||||
Files can belong to multiple categories. **More specific categories come first.**
|
||||
|
||||
### Category Hierarchy
|
||||
|
||||
For `my-post.md`:
|
||||
```
|
||||
Categories: ["md", "document"]
|
||||
↑ ↑
|
||||
specific general
|
||||
```
|
||||
|
||||
**Search order:**
|
||||
```
|
||||
1. __file.md.html ← Most specific
|
||||
2. __file.document.html ← More general
|
||||
3. __file.html ← Most general
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Enable debug mode to see template discovery in action:
|
||||
|
||||
```toml
|
||||
# config.toml
|
||||
[server]
|
||||
debug = true
|
||||
```
|
||||
|
||||
**Console output when visiting a page:**
|
||||
```
|
||||
[DEBUG] Template discovery for: blog/my-post.md
|
||||
[DEBUG] Content type: file
|
||||
[DEBUG] Categories: ['md', 'document']
|
||||
[DEBUG] Extension: md
|
||||
[DEBUG]
|
||||
[DEBUG] Searching templates:
|
||||
[DEBUG] 1. templates/blog/my-post.html - NOT FOUND
|
||||
[DEBUG] 2. templates/blog/__file.md.html - FOUND!
|
||||
[DEBUG]
|
||||
[DEBUG] Using template: templates/blog/__file.md.html
|
||||
```
|
||||
|
||||
### Debug Test Page
|
||||
|
||||
Create a test page to understand discovery:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Template Discovery Test"
|
||||
---
|
||||
|
||||
# Current Template Info
|
||||
|
||||
**Current Path:** {{ currentPath }}
|
||||
|
||||
**Styles Loaded:**
|
||||
{% for style in styles %}
|
||||
- {{ style }}
|
||||
{% endfor %}
|
||||
|
||||
**Metadata:**
|
||||
{{ metadata }}
|
||||
```
|
||||
|
||||
Visit this page and check the console to see which template was used.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### No Template Found
|
||||
|
||||
If no template matches:
|
||||
|
||||
**For files:**
|
||||
- Foldsite serves the file directly (download)
|
||||
- Useful for PDFs, images, etc.
|
||||
|
||||
**For folders:**
|
||||
- Returns 404 error
|
||||
- Need at least one `__folder.html` template
|
||||
|
||||
### Hidden Files
|
||||
|
||||
Files/folders starting with `___` are ignored:
|
||||
|
||||
```
|
||||
content/
|
||||
├── post.md ✓ Will be rendered
|
||||
└── ___draft.md ✗ Ignored completely
|
||||
```
|
||||
|
||||
No template search is performed for hidden content.
|
||||
|
||||
### Multiple Extensions
|
||||
|
||||
Only the last extension is considered:
|
||||
|
||||
```
|
||||
my-file.tar.gz → extension: "gz"
|
||||
```
|
||||
|
||||
Not: `tar.gz` or `tar`
|
||||
|
||||
## Style Discovery
|
||||
|
||||
Styles follow **similar logic** to templates but accumulate instead of first-match:
|
||||
|
||||
### Style Search for `blog/post.md`
|
||||
|
||||
**All matching styles are loaded (in order):**
|
||||
```
|
||||
1. /base.css ← Always loaded
|
||||
2. /blog/__file.md.css ← If exists
|
||||
3. /blog/__file.document.css ← If exists
|
||||
4. /__file.md.css ← If exists (from root)
|
||||
5. /blog/post.md.css ← If exists (specific file)
|
||||
```
|
||||
|
||||
**All found styles are included** (not just first match).
|
||||
|
||||
**Rendering:**
|
||||
```html
|
||||
<link rel="stylesheet" href="/styles/base.css">
|
||||
<link rel="stylesheet" href="/styles/blog/__file.md.css">
|
||||
<link rel="stylesheet" href="/styles/__file.md.css">
|
||||
```
|
||||
|
||||
## Optimization Tips
|
||||
|
||||
### 1. Keep Templates at Appropriate Levels
|
||||
|
||||
**Good:**
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← General template
|
||||
└── blog/
|
||||
└── __file.md.html ← Blog-specific customization
|
||||
```
|
||||
|
||||
**Avoid:**
|
||||
```
|
||||
templates/
|
||||
├── post1.html
|
||||
├── post2.html
|
||||
├── post3.html ← Repetitive!
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 2. Use Categories Effectively
|
||||
|
||||
**Good:**
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← For markdown
|
||||
├── __folder.image.html ← For galleries
|
||||
└── __file.document.html ← For all documents
|
||||
```
|
||||
|
||||
**Avoid:**
|
||||
```
|
||||
templates/
|
||||
├── __file.html ← Too generic
|
||||
└── (nothing else)
|
||||
```
|
||||
|
||||
### 3. Understand the Cascade
|
||||
|
||||
Place templates where they make sense:
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
content/
|
||||
├── blog/ → Frequent posts, custom layout
|
||||
├── docs/ → Technical docs, different layout
|
||||
└── about.md → One-off pages
|
||||
```
|
||||
|
||||
**Template structure:**
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← Default for one-off pages
|
||||
├── blog/
|
||||
│ └── __file.md.html ← Blog-specific
|
||||
└── docs/
|
||||
└── __file.md.html ← Docs-specific
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Section-Specific Layouts
|
||||
|
||||
Different sections need different layouts:
|
||||
|
||||
```
|
||||
templates/
|
||||
├── base.html ← Shared wrapper
|
||||
├── __file.md.html ← Default content
|
||||
├── blog/
|
||||
│ ├── __file.md.html ← Blog posts
|
||||
│ └── __folder.md.html ← Blog index
|
||||
├── portfolio/
|
||||
│ ├── __file.md.html ← Project pages
|
||||
│ └── __folder.md.html ← Portfolio grid
|
||||
└── docs/
|
||||
├── __file.md.html ← Documentation pages
|
||||
└── __folder.md.html ← Docs index
|
||||
```
|
||||
|
||||
### Pattern: Gradual Specialization
|
||||
|
||||
Start general, add specificity as needed:
|
||||
|
||||
**Phase 1: MVP**
|
||||
```
|
||||
templates/
|
||||
├── base.html
|
||||
└── __file.md.html
|
||||
```
|
||||
|
||||
**Phase 2: Add Gallery**
|
||||
```
|
||||
templates/
|
||||
├── base.html
|
||||
├── __file.md.html
|
||||
└── __folder.image.html ← New!
|
||||
```
|
||||
|
||||
**Phase 3: Custom Blog**
|
||||
```
|
||||
templates/
|
||||
├── base.html
|
||||
├── __file.md.html
|
||||
├── __folder.image.html
|
||||
└── blog/
|
||||
└── __file.md.html ← New!
|
||||
```
|
||||
|
||||
### Pattern: Override Single Page
|
||||
|
||||
Override one specific page:
|
||||
|
||||
```
|
||||
templates/
|
||||
├── __file.md.html ← All markdown
|
||||
└── index.html ← Special homepage
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Template Not Being Used
|
||||
|
||||
**Check:**
|
||||
1. **File name** - Is it exactly right?
|
||||
2. **Location** - Is it in the right directory?
|
||||
3. **Extension** - `.html`, not `.htm` or `.jinja`
|
||||
4. **Debug mode** - What does the console say?
|
||||
|
||||
**Debug:**
|
||||
```bash
|
||||
# Enable debug
|
||||
[server]
|
||||
debug = true
|
||||
|
||||
# Restart and check console output
|
||||
```
|
||||
|
||||
### Wrong Template Used
|
||||
|
||||
**Likely cause:** More specific template exists
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Want: templates/__file.md.html
|
||||
Using: templates/blog/__file.md.html ← More specific!
|
||||
```
|
||||
|
||||
**Solution:** Update the more specific template, or remove it.
|
||||
|
||||
### Styles Not Loading
|
||||
|
||||
**Check:**
|
||||
1. **Style file exists** in `styles/` directory
|
||||
2. **Path matches** template expectations
|
||||
3. **Browser dev tools** - Are styles being requested?
|
||||
|
||||
**Remember:** Unlike templates, **all matching styles load**.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Template System Overview](index.md)** - Basics of templates
|
||||
- **[Template Helpers](template-helpers.md)** - Functions available in templates
|
||||
- **[Recipes](../recipes/)** - Working examples
|
||||
- **[Styles Guide](../styles/)** - CSS cascade system
|
||||
|
||||
Understanding template discovery gives you complete control over your site's presentation. Use it wisely!
|
793
docs/content/templates/template-helpers.md
Normal file
793
docs/content/templates/template-helpers.md
Normal file
@ -0,0 +1,793 @@
|
||||
---
|
||||
version: "1.0"
|
||||
date: "2025-01-15"
|
||||
author: "DWS Foldsite Team"
|
||||
title: "Template Helper Functions"
|
||||
description: "Complete reference for all Jinja2 helper functions in Foldsite"
|
||||
summary: "Comprehensive API documentation for Foldsite's template helper functions - discover content, navigate your site, and build dynamic features."
|
||||
quick_tips:
|
||||
- "All helpers are automatically available in every template"
|
||||
- "Helpers return Python objects you can loop over and filter"
|
||||
- "Most helpers are cached for performance - safe to call multiple times"
|
||||
---
|
||||
|
||||
# Template Helper Functions
|
||||
|
||||
Foldsite provides powerful helper functions you can use in any template. These functions access your content dynamically, enabling features like recent posts, navigation menus, breadcrumbs, and more.
|
||||
|
||||
## Content Discovery
|
||||
|
||||
### get_folder_contents(path)
|
||||
|
||||
Get all files and folders in a directory with rich metadata.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str, optional) - Relative path from content root. Defaults to current directory.
|
||||
|
||||
**Returns:** List of `TemplateFile` objects with attributes:
|
||||
- `name` (str) - Filename with extension
|
||||
- `path` (str) - Relative path from content root
|
||||
- `proper_name` (str) - Filename without extension
|
||||
- `extension` (str) - File extension (`.md`, `.jpg`, etc.)
|
||||
- `categories` (list[str]) - File categories (`['document']`, `['image']`, etc.)
|
||||
- `date_modified` (str) - Last modified date (`YYYY-MM-DD`)
|
||||
- `date_created` (str) - Creation date (`YYYY-MM-DD`)
|
||||
- `size_kb` (int) - File size in kilobytes
|
||||
- `metadata` (dict | None) - Markdown frontmatter or image EXIF data
|
||||
- `dir_item_count` (int) - Number of items if it's a directory
|
||||
- `is_dir` (bool) - True if it's a directory
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<h2>Files in this folder:</h2>
|
||||
<ul>
|
||||
{% for file in get_folder_contents(currentPath) %}
|
||||
<li>
|
||||
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
|
||||
({{ file.size_kb }} KB, modified {{ file.date_modified }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Sort and filter:**
|
||||
|
||||
```jinja
|
||||
{# Sort by date, newest first #}
|
||||
{% for file in get_folder_contents()|sort(attribute='date_created', reverse=True) %}
|
||||
...
|
||||
{% endfor %}
|
||||
|
||||
{# Filter to only documents #}
|
||||
{% for file in get_folder_contents() %}
|
||||
{% if 'document' in file.categories %}
|
||||
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### get_sibling_content_files(path)
|
||||
|
||||
Get files in the same directory as the current page.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str, optional) - Relative path. Defaults to current directory.
|
||||
|
||||
**Returns:** List of tuples `(filename, relative_path)`
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<nav class="sibling-nav">
|
||||
<h3>Other pages in this section:</h3>
|
||||
{% for name, path in get_sibling_content_files(currentPath) %}
|
||||
<a href="/{{ path }}"
|
||||
{% if path == currentPath %}class="active"{% endif %}>
|
||||
{{ name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
```
|
||||
|
||||
### get_sibling_content_folders(path)
|
||||
|
||||
Get folders in the same directory as the current page.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str, optional) - Relative path. Defaults to current directory.
|
||||
|
||||
**Returns:** List of tuples `(folder_name, relative_path)`
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<nav class="folder-nav">
|
||||
<h3>Sections:</h3>
|
||||
{% for name, path in get_sibling_content_folders(currentPath) %}
|
||||
<a href="/{{ path }}">{{ name }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
```
|
||||
|
||||
### get_text_document_preview(path)
|
||||
|
||||
Get a preview (first 100 characters) of a text document.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str) - Relative path to the document
|
||||
|
||||
**Returns:** String (first 100 characters of the file)
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
{% for file in get_folder_contents() %}
|
||||
{% if 'document' in file.categories %}
|
||||
<article>
|
||||
<h3><a href="/{{ file.path }}">{{ file.proper_name }}</a></h3>
|
||||
<p>{{ get_text_document_preview(file.path) }}...</p>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### get_rendered_markdown(path)
|
||||
|
||||
Get fully rendered markdown content without Jinja2 templating. Perfect for displaying markdown files (like `index.md`) within folder views or embedding content from one markdown file into a template.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str) - Relative path to the markdown file
|
||||
|
||||
**Returns:** Dictionary with:
|
||||
- `html` (str | None) - Rendered HTML content from markdown
|
||||
- `metadata` (dict | None) - Frontmatter metadata from the markdown file
|
||||
- `exists` (bool) - True if file was found and rendered successfully
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
{# Display index.md in a folder view #}
|
||||
{% set index_path = (currentPath + '/index.md') if currentPath else 'index.md' %}
|
||||
{% set index = get_rendered_markdown(index_path) %}
|
||||
{% if index.exists %}
|
||||
<section class="folder-index">
|
||||
{{ index.html | safe }}
|
||||
{% if index.metadata.author %}
|
||||
<p class="author">By {{ index.metadata.author }}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Use in folder templates:**
|
||||
|
||||
```jinja
|
||||
{# Show index.md content at the top of a folder listing #}
|
||||
<div class="folder-view">
|
||||
{% set index = get_rendered_markdown(currentPath + '/index.md') %}
|
||||
{% if index.exists %}
|
||||
<div class="folder-introduction">
|
||||
{{ index.html | safe }}
|
||||
<hr>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Then show other files in the folder #}
|
||||
<div class="folder-contents">
|
||||
{% for file in get_folder_contents(currentPath) %}
|
||||
{% if file.name != 'index.md' %}
|
||||
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Embed content from another file:**
|
||||
|
||||
```jinja
|
||||
{# Include shared content from another markdown file #}
|
||||
{% set about = get_rendered_markdown('about/team-bio.md') %}
|
||||
{% if about.exists %}
|
||||
<aside class="team-bio">
|
||||
{{ about.html | safe }}
|
||||
</aside>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### get_markdown_metadata(path)
|
||||
|
||||
Get metadata (frontmatter) from a markdown file **without rendering the content**. Perfect for displaying static markdown metadata in any location - like showing post titles, descriptions, or custom fields without the overhead of rendering the full markdown.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (str) - Relative path to the markdown file
|
||||
|
||||
**Returns:** Dictionary with:
|
||||
- `metadata` (dict | None) - Frontmatter metadata from the markdown file
|
||||
- `exists` (bool) - True if file was found successfully
|
||||
- `error` (str, optional) - Error message if reading failed
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
{# Display metadata from a specific post without rendering it #}
|
||||
{% set post_meta = get_markdown_metadata('blog/my-awesome-post.md') %}
|
||||
{% if post_meta.exists %}
|
||||
<div class="featured-post">
|
||||
<h2>{{ post_meta.metadata.title }}</h2>
|
||||
<p class="description">{{ post_meta.metadata.description }}</p>
|
||||
<p class="date">Published: {{ post_meta.metadata.date }}</p>
|
||||
<div class="tags">
|
||||
{% for tag in post_meta.metadata.tags %}
|
||||
<span class="tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/blog/my-awesome-post.md">Read more →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Build a custom post grid using metadata only:**
|
||||
|
||||
```jinja
|
||||
{# Create cards showing metadata from multiple posts #}
|
||||
<div class="post-grid">
|
||||
{% for post_path in ['blog/intro.md', 'blog/tutorial.md', 'blog/advanced.md'] %}
|
||||
{% set meta = get_markdown_metadata(post_path) %}
|
||||
{% if meta.exists %}
|
||||
<article class="post-card">
|
||||
<h3>{{ meta.metadata.title }}</h3>
|
||||
<time datetime="{{ meta.metadata.date }}">{{ meta.metadata.date }}</time>
|
||||
{% if meta.metadata.author %}
|
||||
<p class="author">By {{ meta.metadata.author }}</p>
|
||||
{% endif %}
|
||||
{% if meta.metadata.excerpt %}
|
||||
<p>{{ meta.metadata.excerpt }}</p>
|
||||
{% endif %}
|
||||
<a href="/{{ post_path }}">Read full post</a>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Show custom metadata fields:**
|
||||
|
||||
```jinja
|
||||
{# Display reading time, difficulty, or any custom frontmatter field #}
|
||||
{% set meta = get_markdown_metadata('tutorials/python-basics.md') %}
|
||||
{% if meta.exists %}
|
||||
<div class="tutorial-info">
|
||||
<h2>{{ meta.metadata.title }}</h2>
|
||||
{% if meta.metadata.difficulty %}
|
||||
<span class="badge">{{ meta.metadata.difficulty }}</span>
|
||||
{% endif %}
|
||||
{% if meta.metadata.reading_time %}
|
||||
<span class="time">⏱ {{ meta.metadata.reading_time }} min read</span>
|
||||
{% endif %}
|
||||
{% if meta.metadata.prerequisites %}
|
||||
<div class="prerequisites">
|
||||
<strong>Prerequisites:</strong>
|
||||
<ul>
|
||||
{% for prereq in meta.metadata.prerequisites %}
|
||||
<li>{{ prereq }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Performance tip:** Use `get_markdown_metadata` instead of `get_rendered_markdown` when you only need frontmatter data. It's much faster since it doesn't process the markdown content.
|
||||
|
||||
## Blog Functions
|
||||
|
||||
### get_recent_posts(limit, folder)
|
||||
|
||||
Get recent blog posts sorted by date (newest first).
|
||||
|
||||
**Parameters:**
|
||||
- `limit` (int, optional) - Maximum number of posts. Default: 5
|
||||
- `folder` (str, optional) - Search within specific folder. Default: "" (search everywhere)
|
||||
|
||||
**Returns:** List of post dictionaries with:
|
||||
- `title` (str) - From frontmatter or filename
|
||||
- `date` (str) - From frontmatter
|
||||
- `path` (str) - Relative path to post
|
||||
- `url` (str) - Full URL to post
|
||||
- `metadata` (dict) - Full frontmatter including tags, description, etc.
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<section class="recent-posts">
|
||||
<h2>Recent Posts</h2>
|
||||
{% for post in get_recent_posts(limit=5, folder='blog') %}
|
||||
<article>
|
||||
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
|
||||
<time datetime="{{ post.date }}">{{ post.date }}</time>
|
||||
{% if post.metadata.description %}
|
||||
<p>{{ post.metadata.description }}</p>
|
||||
{% endif %}
|
||||
{% if post.metadata.tags %}
|
||||
<div class="tags">
|
||||
{% for tag in post.metadata.tags %}
|
||||
<span class="tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
```
|
||||
|
||||
**Search in specific folder:**
|
||||
|
||||
```jinja
|
||||
{# Only posts from blog/tutorials/ #}
|
||||
{% for post in get_recent_posts(limit=10, folder='blog/tutorials') %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### get_posts_by_tag(tag, limit)
|
||||
|
||||
Get posts filtered by a specific tag.
|
||||
|
||||
**Parameters:**
|
||||
- `tag` (str) - Tag to filter by (case-insensitive)
|
||||
- `limit` (int, optional) - Maximum posts. Default: 10
|
||||
|
||||
**Returns:** List of post dictionaries (same format as `get_recent_posts`)
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<h2>Python Posts</h2>
|
||||
{% for post in get_posts_by_tag('python', limit=10) %}
|
||||
<article>
|
||||
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
|
||||
<time>{{ post.date }}</time>
|
||||
</article>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
**Dynamic tag pages:**
|
||||
|
||||
```jinja
|
||||
{# If currentPath is 'tags/python.md' #}
|
||||
{% set tag = currentPath.split('/')[-1].replace('.md', '') %}
|
||||
<h1>Posts tagged: {{ tag }}</h1>
|
||||
{% for post in get_posts_by_tag(tag) %}
|
||||
...
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### get_related_posts(current_post_path, limit)
|
||||
|
||||
Find posts related to the current post based on shared tags.
|
||||
|
||||
**Parameters:**
|
||||
- `current_post_path` (str) - Path to current post
|
||||
- `limit` (int, optional) - Maximum related posts. Default: 3
|
||||
|
||||
**Returns:** List of post dictionaries with an additional `overlap_score` field (number of shared tags)
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
{% set related = get_related_posts(currentPath, limit=3) %}
|
||||
{% if related %}
|
||||
<aside class="related-posts">
|
||||
<h3>You might also like:</h3>
|
||||
{% for post in related %}
|
||||
<article>
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
<small>{{ post.overlap_score }} shared tags</small>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### get_all_tags()
|
||||
|
||||
Get all tags used across the site with post counts.
|
||||
|
||||
**Returns:** List of tag dictionaries with:
|
||||
- `name` (str) - Tag name
|
||||
- `count` (int) - Number of posts with this tag
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<div class="tag-cloud">
|
||||
{% for tag in get_all_tags() %}
|
||||
<a href="/tags/{{ tag.name|lower }}"
|
||||
class="tag"
|
||||
style="font-size: {{ 0.8 + (tag.count * 0.1) }}em;">
|
||||
{{ tag.name }} ({{ tag.count }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Sort by popularity:**
|
||||
|
||||
```jinja
|
||||
{% for tag in get_all_tags()|sort(attribute='count', reverse=True) %}
|
||||
<a href="/tags/{{ tag.name|lower }}">
|
||||
{{ tag.name }} <span class="count">{{ tag.count }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### generate_breadcrumbs(current_path)
|
||||
|
||||
Generate breadcrumb navigation based on URL path.
|
||||
|
||||
**Parameters:**
|
||||
- `current_path` (str) - Current page path
|
||||
|
||||
**Returns:** List of breadcrumb dictionaries with:
|
||||
- `title` (str) - Display title (from metadata or derived from path)
|
||||
- `url` (str) - URL to this level
|
||||
- `is_current` (bool) - True if this is the current page
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
{% for crumb in generate_breadcrumbs(currentPath) %}
|
||||
{% if not crumb.is_current %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||
{% else %}
|
||||
<span aria-current="page">{{ crumb.title }}</span>
|
||||
{% endif %}
|
||||
{% if not loop.last %} / {% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
```
|
||||
|
||||
**Styled example:**
|
||||
|
||||
```jinja
|
||||
<ol class="breadcrumb">
|
||||
{% for crumb in generate_breadcrumbs(currentPath) %}
|
||||
<li class="breadcrumb-item {% if crumb.is_current %}active{% endif %}">
|
||||
{% if not crumb.is_current %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||
{% else %}
|
||||
{{ crumb.title }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
```
|
||||
|
||||
### get_navigation_items(max_items)
|
||||
|
||||
Get top-level pages and folders for site navigation.
|
||||
|
||||
**Parameters:**
|
||||
- `max_items` (int, optional) - Maximum items to return. Default: 10
|
||||
|
||||
**Returns:** List of navigation dictionaries with:
|
||||
- `title` (str) - Display title (from metadata or filename)
|
||||
- `url` (str) - URL to page/folder
|
||||
- `path` (str) - Relative path
|
||||
- `is_folder` (bool, optional) - True if it's a folder
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<nav class="main-nav">
|
||||
<a href="/">Home</a>
|
||||
{% for item in get_navigation_items(max_items=10) %}
|
||||
<a href="{{ item.url }}"
|
||||
{% if currentPath.startswith(item.path) %}class="active"{% endif %}>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
```
|
||||
|
||||
**With icons for folders:**
|
||||
|
||||
```jinja
|
||||
{% for item in get_navigation_items() %}
|
||||
<a href="{{ item.url }}">
|
||||
{% if item.is_folder %}📁{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## Gallery Functions
|
||||
|
||||
### get_photo_albums()
|
||||
|
||||
Get all photo gallery directories (folders containing mostly images).
|
||||
|
||||
**Returns:** List of album dictionaries with:
|
||||
- `name` (str) - Album name
|
||||
- `path` (str) - Relative path
|
||||
- `url` (str) - Full URL
|
||||
- `image_count` (int) - Number of images
|
||||
- `total_files` (int) - Total files in album
|
||||
|
||||
**Example:**
|
||||
|
||||
```jinja
|
||||
<div class="photo-albums">
|
||||
<h2>Photo Galleries</h2>
|
||||
{% for album in get_photo_albums() %}
|
||||
<a href="{{ album.url }}" class="album-card">
|
||||
<h3>{{ album.name }}</h3>
|
||||
<p>{{ album.image_count }} photos</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Built-in Jinja2 Filters
|
||||
|
||||
In addition to Foldsite helpers, you can use standard Jinja2 filters:
|
||||
|
||||
### String Filters
|
||||
|
||||
```jinja
|
||||
{{ "hello"|upper }} {# HELLO #}
|
||||
{{ "HELLO"|lower }} {# hello #}
|
||||
{{ "hello world"|title }} {# Hello World #}
|
||||
{{ "hello world"|capitalize }} {# Hello world #}
|
||||
{{ " text "|trim }} {# text #}
|
||||
{{ "hello"|reverse }} {# olleh #}
|
||||
```
|
||||
|
||||
### List Filters
|
||||
|
||||
```jinja
|
||||
{{ items|length }} {# Count items #}
|
||||
{{ items|first }} {# First item #}
|
||||
{{ items|last }} {# Last item #}
|
||||
{{ items|join(', ') }} {# Join with comma #}
|
||||
{{ items|sort }} {# Sort list #}
|
||||
{{ items|sort(attribute='date') }} {# Sort by attribute #}
|
||||
{{ items|reverse }} {# Reverse list #}
|
||||
{{ items|unique }} {# Remove duplicates #}
|
||||
```
|
||||
|
||||
### Other Useful Filters
|
||||
|
||||
```jinja
|
||||
{{ value|default('N/A') }} {# Default if falsy #}
|
||||
{{ html|safe }} {# Don't escape HTML #}
|
||||
{{ number|round(2) }} {# Round to 2 decimals #}
|
||||
{{ date|replace('-', '/') }} {# Replace strings #}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Blog Homepage with Recent Posts
|
||||
|
||||
```jinja
|
||||
<main>
|
||||
<h1>Welcome to My Blog</h1>
|
||||
|
||||
<section class="recent-posts">
|
||||
{% for post in get_recent_posts(limit=10) %}
|
||||
<article class="post-card">
|
||||
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
|
||||
<time datetime="{{ post.date }}">{{ post.date }}</time>
|
||||
|
||||
{% if post.metadata.description %}
|
||||
<p>{{ post.metadata.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if post.metadata.tags %}
|
||||
<div class="tags">
|
||||
{% for tag in post.metadata.tags %}
|
||||
<a href="/tags/{{ tag|lower }}" class="tag">{{ tag }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<aside class="sidebar">
|
||||
<h3>Popular Tags</h3>
|
||||
<div class="tag-cloud">
|
||||
{% for tag in get_all_tags()|sort(attribute='count', reverse=True)[:10] %}
|
||||
<a href="/tags/{{ tag.name|lower }}">{{ tag.name }} ({{ tag.count }})</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
```
|
||||
|
||||
### Pattern: Folder Index with Previews
|
||||
|
||||
```jinja
|
||||
<div class="folder-index">
|
||||
<h1>{{ currentPath.split('/')[-1]|title }}</h1>
|
||||
|
||||
<nav class="breadcrumbs">
|
||||
{% for crumb in generate_breadcrumbs(currentPath) %}
|
||||
{% if not crumb.is_current %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.title }}</a> /
|
||||
{% else %}
|
||||
{{ crumb.title }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{# Show index.md content if it exists #}
|
||||
{% set index = get_rendered_markdown(currentPath + '/index.md') %}
|
||||
{% if index.exists %}
|
||||
<section class="folder-introduction">
|
||||
{{ index.html | safe }}
|
||||
<hr>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="content-grid">
|
||||
{% for file in get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
|
||||
{% if 'document' in file.categories and file.name != 'index.md' %}
|
||||
<div class="content-card">
|
||||
<h3><a href="/{{ file.path }}">{{ file.proper_name }}</a></h3>
|
||||
<p class="date">{{ file.date_created }}</p>
|
||||
{% if file.metadata and file.metadata.description %}
|
||||
<p>{{ file.metadata.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern: Photo Gallery with EXIF Data
|
||||
|
||||
```jinja
|
||||
<div class="gallery">
|
||||
{% set photos = get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
|
||||
|
||||
<h1>{{ currentPath.split('/')[-1]|title }}</h1>
|
||||
<p>{{ photos|length }} photos</p>
|
||||
|
||||
<div class="photo-grid">
|
||||
{% for photo in photos %}
|
||||
{% if 'image' in photo.categories %}
|
||||
<figure class="photo">
|
||||
<a href="/download/{{ photo.path }}">
|
||||
<img src="/download/{{ photo.path }}?max_width=600"
|
||||
alt="{{ photo.name }}"
|
||||
loading="lazy">
|
||||
</a>
|
||||
<figcaption>
|
||||
{% if photo.metadata and photo.metadata.exif %}
|
||||
<p>{{ photo.metadata.exif.DateTimeOriginal }}</p>
|
||||
{% if photo.metadata.exif.Model %}
|
||||
<p>{{ photo.metadata.exif.Model }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern: Documentation with Sibling Pages
|
||||
|
||||
```jinja
|
||||
<article class="doc-page">
|
||||
<nav class="doc-sidebar">
|
||||
<h3>In This Section:</h3>
|
||||
<ul>
|
||||
{% for name, path in get_sibling_content_files(currentPath) %}
|
||||
<li>
|
||||
<a href="/{{ path }}"
|
||||
{% if path == currentPath %}class="active"{% endif %}>
|
||||
{{ name.replace('.md', '')|replace('-', ' ')|title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="doc-content">
|
||||
<nav class="breadcrumbs">
|
||||
{% for crumb in generate_breadcrumbs(currentPath) %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||
{% if not loop.last %} › {% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{{ content|safe }}
|
||||
|
||||
{% set related = get_related_posts(currentPath, limit=3) %}
|
||||
{% if related %}
|
||||
<aside class="related-docs">
|
||||
<h4>Related Documentation:</h4>
|
||||
<ul>
|
||||
{% for doc in related %}
|
||||
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</aside>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Caching
|
||||
|
||||
Most helper functions are cached automatically. It's safe to call them multiple times:
|
||||
|
||||
```jinja
|
||||
{# These don't cause multiple filesystem scans #}
|
||||
{% set posts = get_recent_posts(limit=5) %}
|
||||
... use posts ...
|
||||
{% set posts_again = get_recent_posts(limit=5) %} {# Cached result #}
|
||||
```
|
||||
|
||||
### Filtering vs. Multiple Calls
|
||||
|
||||
Filter results in templates rather than calling helpers multiple times:
|
||||
|
||||
```jinja
|
||||
✓ Efficient:
|
||||
{% set all_files = get_folder_contents() %}
|
||||
{% set docs = all_files|selectattr('categories', 'contains', 'document') %}
|
||||
{% set images = all_files|selectattr('categories', 'contains', 'image') %}
|
||||
|
||||
✗ Less efficient:
|
||||
{% for file in get_folder_contents() %}
|
||||
{% if 'document' in file.categories %}...{% endif %}
|
||||
{% endfor %}
|
||||
{% for file in get_folder_contents() %} {# Second call #}
|
||||
{% if 'image' in file.categories %}...{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable debug mode to see which templates and helpers are being used:
|
||||
|
||||
```toml
|
||||
# config.toml
|
||||
[server]
|
||||
debug = true
|
||||
```
|
||||
|
||||
Add debug output to templates:
|
||||
|
||||
```jinja
|
||||
{# Show what get_recent_posts returns #}
|
||||
{% set posts = get_recent_posts(limit=3) %}
|
||||
<pre>{{ posts|pprint }}</pre>
|
||||
|
||||
{# Show current variables #}
|
||||
<pre>
|
||||
currentPath: {{ currentPath }}
|
||||
metadata: {{ metadata|pprint }}
|
||||
</pre>
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Template Recipes](../recipes/)** - See these helpers in complete working examples
|
||||
- **[Template System Overview](index.md)** - Understand how templates work
|
||||
- **[Template Discovery](template-discovery.md)** - Learn how Foldsite finds templates
|
||||
|
||||
With these helpers, you can build sophisticated, dynamic sites while keeping your content as simple files and folders!
|
Reference in New Issue
Block a user