794 lines
23 KiB
Markdown
794 lines
23 KiB
Markdown
---
|
||
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!
|