docs refactor
All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 52s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 1m1s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m50s

This commit is contained in:
2025-10-09 18:21:23 -04:00
parent c9a3a21f07
commit ad81d7f3db
39 changed files with 12551 additions and 1037 deletions

View 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>&copy; 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!

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

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