Use Kida's built-in{% cache %}directive for manual control over expensive template operations.
When to Use Fragment Caching
Cache fragments when you have:
- Expensive computations (navigation trees, related posts)
- Complex data processing (statistics, aggregations)
- Repeated calculations across pages
Basic Caching
Simple Cache
{% cache "sidebar-nav" %}
{{ build_nav_tree(site.pages) }}
{% end %}
Dynamic Cache Keys
Use expressions to create unique cache keys:
{% cache "related-" ~ post.id %}
{% let related = site.pages
|> where('tags', post.tags[0])
|> take(5) %}
<aside>
{% for item in related %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% end %}
</aside>
{% end %}
Cache with TTL
Fragment cache uses a global TTL configured at the environment level. All cached fragments share the same expiration time set inbengal.yaml:
{% cache "stats" %}
{{ calculate_site_stats() }}
{% end %}
Note: Per-key TTL (e.g.,ttl="1h") is not currently supported. All fragments use the environment-levelfragment_ttlsetting. See Configuration to set the global TTL.
Common Use Cases
Related Posts
{% cache "related-posts-" ~ page.id %}
{% let related = site.pages
|> where('type', 'blog')
|> where('tags', page.tags[0] ?? '')
|> where('id', '!=', page.id)
|> sort_by('date', reverse=true)
|> take(5) %}
{% if related %}
<aside class="related-posts">
<h2>Related Posts</h2>
<ul>
{% for item in related %}
<li>
<a href="{{ item.url }}">{{ item.title }}</a>
<span>{{ item.date | days_ago }} days ago</span>
</li>
{% end %}
</ul>
</aside>
{% end %}
{% end %}
Navigation Tree
{% cache "nav-tree-" ~ site.nav_version %}
{% let nav = get_nav_tree(page) %}
<nav>
{% for item in nav %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% end %}
</nav>
{% end %}
Site Statistics
{% cache "site-stats" %}
{% let total_posts = site.pages |> where('type', 'blog') |> length %}
{% let total_docs = site.pages |> where('type', 'doc') |> length %}
{% let total_tags = site.tags |> length %}
<div class="stats">
<div class="stat">
<span class="value">{{ total_posts }}</span>
<span class="label">Posts</span>
</div>
<div class="stat">
<span class="value">{{ total_docs }}</span>
<span class="label">Docs</span>
</div>
<div class="stat">
<span class="value">{{ total_tags }}</span>
<span class="label">Tags</span>
</div>
</div>
{% end %}
Tag Cloud
{% cache "tag-cloud" %}
{% let tags = site.tags
|> items()
|> sort_by('count', reverse=true)
|> take(20) %}
<div class="tag-cloud">
{% for tag in tags %}
<a href="{{ tag_url(tag.name) }}" class="tag tag-{{ tag.count }}">
{{ tag.name }}
</a>
{% end %}
</div>
{% end %}
Cache Invalidation
Version-Based
{% cache "sidebar-" ~ site.nav_version %}
{# Cache invalidates when nav_version changes #}
{{ build_sidebar() }}
{% end %}
Content-Based
{% cache "post-meta-" ~ post.id ~ "-" ~ post.updated_at %}
{# Cache invalidates when post is updated #}
<div class="post-meta">
<time>{{ post.date | dateformat('%B %d, %Y') }}</time>
</div>
{% end %}
Manual Invalidation
Fragment cache is stored in memory and persists only during the build process. To clear it:
During build:
- Fragment cache is automatically cleared when you restart the build process
- Use
bengal clean --cacheto clear all caches (including bytecode cache)
Note: The fragment cache is separate from the bytecode cache:
- Fragment cache: In-memory LRU cache for
{% cache %}block outputs (cleared on process restart) - Bytecode cache: Disk cache at
.bengal/cache/kida/for compiled template bytecode (persists across builds)
# Clear all caches (output + bytecode cache)
bengal clean --cache
# Clear only bytecode cache (fragment cache is in-memory)
rm -rf .bengal/cache/kida/
Best Practices
Cache Expensive Operations
{# ✅ Expensive: Cache it #}
{% cache "expensive-calculation" %}
{% let result = expensive_function(site.pages) %}
{{ result }}
{% end %}
{# ❌ Cheap: Don't cache #}
{{ page.title }}
Configure TTL Appropriately
Set the global TTL inbengal.yamlbased on your content update frequency:
kida:
fragment_ttl: 300.0 # 5 minutes - good for frequently changing content
# fragment_ttl: 3600.0 # 1 hour - good for rarely changing content
{# All fragments use the same TTL from configuration #}
{% cache "recent-posts" %}
{{ get_recent_posts() }}
{% end %}
{% cache "site-stats" %}
{{ calculate_stats() }}
{% end %}
Don't Over-Cache
{# ❌ Don't cache simple variables #}
{% cache "title" %}
{{ page.title }}
{% end %}
{# ✅ Cache expensive operations #}
{% cache "related-posts" %}
{{ calculate_related_posts(page) }}
{% end %}
Configuration
Configure cache settings inbengal.yaml:
kida:
fragment_cache_size: 1000 # Max cached fragments (LRU eviction)
fragment_ttl: 300.0 # Global TTL in seconds for all fragments (5 min)
Cache behavior:
fragment_cache_size: Maximum number of cached fragments. When exceeded, least recently used entries are evicted.fragment_ttl: Time-to-live in seconds applied to all cached fragments. Fragments expire after this duration regardless of access.- Fragment cache is in-memory only and cleared when the build process ends.
Debugging
Fragment cache statistics:
Fragment cache statistics are available programmatically via the Kida environment'scache_info()method. The fragment cache is in-memory and cleared on each build.
Bytecode cache:
The bytecode cache (compiled template bytecode) is stored on disk:
# Check bytecode cache directory
ls -la .bengal/cache/kida/
# Build with verbose output
bengal build --verbose
Note: Fragment cache (for{% cache %}blocks) and bytecode cache (for compiled templates) are separate systems with different storage locations and purposes.
Complete Example
{% extends "baseof.html" %}
{% let post = page %}
{% block content %}
<article class="blog-post">
<header>
<h1>{{ post.title }}</h1>
{% cache "post-meta-" ~ post.id %}
<div class="post-meta">
<time>{{ post.date | dateformat('%B %d, %Y') }}</time>
<span>{{ post.reading_time }} min read</span>
</div>
{% end %}
</header>
<div class="content">
{{ post.content | safe }}
</div>
{% cache "related-posts-" ~ post.id %}
{% let related = site.pages
|> where('type', 'blog')
|> where('tags', post.tags[0] | default(''))
|> where('id', '!=', post.id)
|> sort_by('date', reverse=true)
|> take(5) %}
{% if related %}
<aside class="related-posts">
<h2>Related Posts</h2>
<ul>
{% for item in related %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% end %}
</ul>
</aside>
{% end %}
{% end %}
</article>
{% endblock %}
{% block sidebar %}
{% cache "sidebar-nav-" ~ site.nav_version %}
{{ build_sidebar_nav(site.pages) }}
{% end %}
{% cache "tag-cloud" %}
{{ build_tag_cloud(site.tags) }}
{% end %}
{% endblock %}