20 KiB
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 Recipes | Ready-to-use examples for common Foldsite patterns | Complete, working examples for building blogs, photo galleries, documentation sites, and more with Foldsite. |
|
Template Recipes
Ready-to-use templates and patterns for common Foldsite use cases. Copy these examples and customize them for your needs.
Recipe Collection
Blog Site
Complete blog setup with recent posts, tag filtering, and related content.
Photo Gallery
Beautiful image galleries with EXIF data, breadcrumbs, and responsive layout.
Documentation Site
Hierarchical documentation with navigation, breadcrumbs, and sibling pages.
Portfolio Site
Project showcase with custom layouts per project.
Recipe 1: Personal Blog
A complete blog with post listings, tags, and related posts.
Directory Structure
my-blog/
├── content/
│ ├── index.md
│ ├── about.md
│ └── posts/
│ ├── 2024-01-15-first-post.md
│ ├── 2024-02-20-python-tips.md
│ └── 2024-03-10-web-development.md
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── __file.md.html
│ └── posts/
│ ├── __file.md.html
│ └── __folder.md.html
└── styles/
├── base.css
└── posts/
└── __file.md.css
Post Frontmatter Format
---
title: "My First Blog Post"
date: "2024-01-15"
author: "Your Name"
tags: ["python", "tutorial", "beginners"]
description: "A brief introduction to Python for beginners"
---
# Your content here...
Template: base.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 Blog</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
<header>
<nav class="main-nav">
<a href="/" class="logo">My Blog</a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/posts">Posts</a>
<a href="/about.md">About</a>
</div>
</nav>
</header>
<main class="container">
{{ content|safe }}
</main>
<footer>
<p>© 2025 My Blog. Built with <a href="https://github.com/DWSresearch/foldsite">Foldsite</a>.</p>
</footer>
</body>
</html>
Template: index.html
(Homepage)
<div class="homepage">
<section class="hero">
<h1>Welcome to My Blog</h1>
<p>Thoughts on coding, life, and everything in between.</p>
</section>
<section class="recent-posts">
<h2>Recent Posts</h2>
<div class="post-grid">
{% for post in get_recent_posts(limit=6, folder='posts') %}
<article class="post-card">
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<time datetime="{{ post.date }}">{{ post.date }}</time>
{% if post.metadata.description %}
<p class="excerpt">{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags[:3] %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
<a href="/posts" class="view-all">View All Posts →</a>
</section>
<aside class="sidebar">
<section class="popular-tags">
<h3>Popular Tags</h3>
<div class="tag-cloud">
{% for tag in get_all_tags()|sort(attribute='count', reverse=True)[:15] %}
<a href="/tags/{{ tag.name|lower }}.md"
class="tag"
style="font-size: {{ 0.9 + (tag.count * 0.15) }}rem;">
{{ tag.name }}
</a>
{% endfor %}
</div>
</section>
</aside>
</div>
Template: posts/__file.md.html
(Blog Post)
<article class="blog-post">
<header class="post-header">
{% if metadata %}
<h1>{{ metadata.title }}</h1>
<div class="post-meta">
<time datetime="{{ metadata.date }}">{{ metadata.date }}</time>
{% if metadata.author %}
<span class="author">by {{ metadata.author }}</span>
{% endif %}
</div>
{% if metadata.tags %}
<div class="post-tags">
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
{% endif %}
</header>
<div class="post-content">
{{ content|safe }}
</div>
<footer class="post-footer">
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<section class="related-posts">
<h3>Related Posts</h3>
<div class="related-grid">
{% for post in related %}
<a href="{{ post.url }}" class="related-card">
<h4>{{ post.title }}</h4>
<time>{{ post.date }}</time>
</a>
{% endfor %}
</div>
</section>
{% endif %}
<nav class="post-navigation">
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
{% set current_index = namespace(value=-1) %}
{% for idx, (name, path) in enumerate(siblings) %}
{% if path == currentPath %}
{% set current_index.value = idx %}
{% endif %}
{% endfor %}
{% if current_index.value > 0 %}
{% set prev = siblings[current_index.value - 1] %}
<a href="/{{ prev[1] }}" class="nav-prev">← {{ prev[0].replace('.md', '') }}</a>
{% endif %}
{% if current_index.value < siblings|length - 1 %}
{% set next = siblings[current_index.value + 1] %}
<a href="/{{ next[1] }}" class="nav-next">{{ next[0].replace('.md', '') }} →</a>
{% endif %}
{% endif %}
</nav>
</footer>
</article>
Template: posts/__folder.md.html
(Post Index)
<div class="post-index">
<header class="index-header">
<h1>All Posts</h1>
<p>{{ get_recent_posts(limit=1000, folder='posts')|length }} posts and counting</p>
</header>
<div class="post-list">
{% for post in get_recent_posts(limit=100, folder='posts') %}
<article class="post-item">
<time datetime="{{ post.date }}">{{ post.date }}</time>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
{% if post.metadata.description %}
<p class="description">{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
</div>
Recipe 2: Photo Gallery
Beautiful, responsive photo galleries with EXIF data extraction.
Directory Structure
photo-site/
├── content/
│ ├── index.md
│ └── galleries/
│ ├── vacation-2024/
│ │ ├── IMG_001.jpg
│ │ ├── IMG_002.jpg
│ │ └── IMG_003.jpg
│ └── family/
│ └── photos...
├── templates/
│ ├── base.html
│ └── galleries/
│ └── __folder.image.html
└── styles/
├── base.css
└── galleries/
└── __folder.image.css
Template: galleries/__folder.image.html
<div class="photo-gallery">
<header class="gallery-header">
<nav class="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 %}
</nav>
<h1>{{ currentPath.split('/')[-1]|replace('-', ' ')|title }}</h1>
{% set photos = get_folder_contents(currentPath)|selectattr('categories', 'contains', 'image') %}
<p class="photo-count">{{ photos|list|length }} photos</p>
{# Show sibling galleries #}
{% set sibling_folders = get_sibling_content_folders(currentPath) %}
{% if sibling_folders %}
<nav class="gallery-nav">
<h3>Other Galleries:</h3>
{% for name, path in sibling_folders %}
<a href="/{{ path }}">{{ name|replace('-', ' ')|title }}</a>
{% endfor %}
</nav>
{% endif %}
</header>
<div class="photo-grid">
{% for photo in photos|list|sort(attribute='date_created', reverse=True) %}
<figure class="photo-item">
<a href="/download/{{ photo.path }}" class="photo-link">
<img src="/download/{{ photo.path }}?max_width=800"
alt="{{ photo.name }}"
loading="lazy"
width="{{ photo.metadata.width if photo.metadata else 800 }}"
height="{{ photo.metadata.height if photo.metadata else 600 }}">
</a>
{% if photo.metadata and photo.metadata.exif %}
<figcaption class="photo-caption">
{% if photo.metadata.exif.DateTimeOriginal %}
<time>{{ photo.metadata.exif.DateTimeOriginal }}</time>
{% endif %}
{% if photo.metadata.exif.Model %}
<span class="camera">{{ photo.metadata.exif.Model }}</span>
{% endif %}
{% if photo.metadata.exif.LensModel %}
<span class="lens">{{ photo.metadata.exif.LensModel }}</span>
{% endif %}
</figcaption>
{% endif %}
</figure>
{% endfor %}
</div>
</div>
{# Optional: Include a lightbox library #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.js"></script>
<script>
const gallery = new Viewer(document.querySelector('.photo-grid'), {
toolbar: true,
title: true,
navbar: true,
});
</script>
Styles: galleries/__folder.image.css
.photo-gallery {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.gallery-header {
margin-bottom: 3rem;
text-align: center;
}
.breadcrumbs {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.breadcrumbs a {
color: #0066cc;
text-decoration: none;
}
.photo-count {
color: #888;
font-size: 0.95rem;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.photo-item {
position: relative;
margin: 0;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.photo-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.photo-link {
display: block;
line-height: 0;
}
.photo-item img {
width: 100%;
height: auto;
display: block;
}
.photo-caption {
padding: 0.75rem;
background: white;
font-size: 0.85rem;
color: #666;
}
.photo-caption time {
display: block;
margin-bottom: 0.25rem;
}
.gallery-nav {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #eee;
}
.gallery-nav a {
display: inline-block;
margin: 0.5rem;
padding: 0.5rem 1rem;
background: #f5f5f5;
border-radius: 4px;
text-decoration: none;
color: #333;
}
.gallery-nav a:hover {
background: #e5e5e5;
}
@media (max-width: 768px) {
.photo-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
}
Recipe 3: Documentation Site
Hierarchical documentation with navigation and search.
Directory Structure
docs-site/
├── content/
│ ├── index.md
│ ├── getting-started/
│ │ ├── installation.md
│ │ ├── configuration.md
│ │ └── quickstart.md
│ ├── guides/
│ │ ├── basics.md
│ │ └── advanced.md
│ └── api/
│ ├── overview.md
│ └── reference.md
├── templates/
│ ├── base.html
│ ├── index.html
│ └── __file.md.html
└── styles/
└── base.css
Template: __file.md.html
(Documentation Page)
<div class="docs-layout">
<aside class="docs-sidebar">
<nav class="sidebar-nav">
<h3>Documentation</h3>
<section class="nav-section">
<h4>Getting Started</h4>
<ul>
<li><a href="/getting-started/installation.md">Installation</a></li>
<li><a href="/getting-started/configuration.md">Configuration</a></li>
<li><a href="/getting-started/quickstart.md">Quick Start</a></li>
</ul>
</section>
<section class="nav-section">
<h4>Guides</h4>
<ul>
<li><a href="/guides/basics.md">Basics</a></li>
<li><a href="/guides/advanced.md">Advanced</a></li>
</ul>
</section>
<section class="nav-section">
<h4>API Reference</h4>
<ul>
<li><a href="/api/overview.md">Overview</a></li>
<li><a href="/api/reference.md">Reference</a></li>
</ul>
</section>
</nav>
{# Show sibling pages automatically #}
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
<nav class="page-nav">
<h4>On This Page:</h4>
<ul>
{% for name, path in siblings %}
<li>
<a href="/{{ path }}"
{% if path == currentPath %}class="active"{% endif %}>
{{ name.replace('.md', '')|replace('-', ' ')|title }}
</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</aside>
<main class="docs-content">
<nav class="breadcrumbs">
{% for crumb in generate_breadcrumbs(currentPath) %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% if not loop.last %} › {% endif %}
{% endfor %}
</nav>
<article class="documentation">
{% if metadata and metadata.title %}
<h1>{{ metadata.title }}</h1>
{% endif %}
{{ content|safe }}
</article>
<footer class="docs-footer">
{# Previous/Next navigation #}
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
<nav class="pagination">
{% set current_index = namespace(value=-1) %}
{% for idx in range(siblings|length) %}
{% if siblings[idx][1] == currentPath %}
{% set current_index.value = idx %}
{% endif %}
{% endfor %}
{% if current_index.value > 0 %}
{% set prev = siblings[current_index.value - 1] %}
<a href="/{{ prev[1] }}" class="prev">
← {{ prev[0].replace('.md', '')|replace('-', ' ')|title }}
</a>
{% endif %}
{% if current_index.value >= 0 and current_index.value < siblings|length - 1 %}
{% set next = siblings[current_index.value + 1] %}
<a href="/{{ next[1] }}" class="next">
{{ next[0].replace('.md', '')|replace('-', ' ')|title }} →
</a>
{% endif %}
</nav>
{% endif %}
{# Related pages based on tags #}
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<section class="related-docs">
<h4>Related Documentation:</h4>
<ul>
{% for doc in related %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
</section>
{% endif %}
</footer>
</main>
</div>
Recipe 4: Portfolio Site
Showcase projects with custom layouts.
Template: Portfolio Homepage
<div class="portfolio">
<section class="hero">
<h1>{{ metadata.title if metadata else "My Portfolio" }}</h1>
<p class="tagline">{{ metadata.description if metadata else "Designer & Developer" }}</p>
</section>
<section class="projects">
<h2>Featured Projects</h2>
<div class="project-grid">
{% for folder_name, folder_path in get_sibling_content_folders('projects') %}
{% set project_files = get_folder_contents(folder_path) %}
{% set cover_image = project_files|selectattr('categories', 'contains', 'image')|first %}
<a href="/{{ folder_path }}" class="project-card">
{% if cover_image %}
<img src="/download/{{ cover_image.path }}?max_width=600"
alt="{{ folder_name }}">
{% endif %}
<h3>{{ folder_name|replace('-', ' ')|title }}</h3>
</a>
{% endfor %}
</div>
</section>
</div>
Mixing Recipes
You can combine elements from different recipes:
Blog + Gallery
Add photo galleries to a blog site by including the gallery template in your blog's template directory.
Documentation + Blog
Add a /blog
section to documentation by including blog templates alongside docs templates.
Portfolio + Blog
Showcase projects and share thoughts by combining portfolio and blog patterns.
Customization Tips
- Start with one recipe - Get it working before adding complexity
- Modify styles first - Change colors, fonts, spacing to match your brand
- Adjust layouts gradually - Start with structure, refine as you go
- Add features incrementally - Don't try to implement everything at once
Next Steps
- Template System - Understanding how templates work
- Template Helpers - Complete API reference
- Styles Guide - Styling your site
- Explore Foldsites - See real examples
Copy these recipes, make them your own, and build something amazing!