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