Files
foldsite/docs/content/templates/template-helpers.md
Tanishq Dubey ad81d7f3db
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
docs refactor
2025-10-09 18:21:23 -04:00

23 KiB
Raw Blame History

version, date, author, title, description, summary, quick_tips
version date author title description summary quick_tips
1.0 2025-01-15 DWS Foldsite Team Template Helper Functions Complete reference for all Jinja2 helper functions in Foldsite Comprehensive API documentation for Foldsite's template helper functions - discover content, navigate your site, and build dynamic features.
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:

<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:

{# 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:

<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:

<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:

{% 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:

{# 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:

{# 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:

{# 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:

{# 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:

{# 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:

{# 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:

<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:

{# 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:

<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:

{# 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:

{% 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:

<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:

{% 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:

<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:

<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:

<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:

{% for item in get_navigation_items() %}
    <a href="{{ item.url }}">
        {% if item.is_folder %}📁{% endif %}
        {{ item.title }}
    </a>
{% endfor %}

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:

<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

{{ "hello"|upper }}              {# HELLO #}
{{ "HELLO"|lower }}              {# hello #}
{{ "hello world"|title }}        {# Hello World #}
{{ "hello world"|capitalize }}   {# Hello world #}
{{ "  text  "|trim }}            {# text #}
{{ "hello"|reverse }}            {# olleh #}

List Filters

{{ 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

{{ 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

<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

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

<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:

{# 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:

✓ 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:

# config.toml
[server]
debug = true

Add debug output to templates:

{# 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

With these helpers, you can build sophisticated, dynamic sites while keeping your content as simple files and folders!