Skip to main content

MintyFlaskSSG in practice: templates and photos

A closer look at MintyFlaskSSG's three-layer template architecture and build-time photo processing—two features that demonstrate the project's design philosophy in action.

Features reveal philosophy. When you examine how software handles specific problems—template resolution, image processing, content organisation—you see the decisions that shaped it. MintyFlaskSSG's template architecture and photo integration demonstrate two core beliefs: composition through layers, and processing at build time.

Let me show you how these work in practice.

The three-layer template system

Flask's template loading is straightforward. You give it directories, it searches them in order, and it returns the first match. Simple, predictable, sufficient. MintyFlaskSSG extends this with a three-layer hierarchy that enables composition without complexity.

The layers work like this:

Theme Layer (themes/your-theme/)
    ↓ extends/imports
Mixin Layer (mixins/blog/, mixins/photos/)
    ↓ extends/includes
Core Layer (themes/_core/)

Each layer serves a distinct purpose. Themes handle presentation—colours, typography, layout decisions. Mixins provide functionality—blog features, photo galleries, documentation navigation. Core supplies the foundation—base templates, essential components, structural patterns.

This maps directly to Flask's ChoiceLoader:

from jinja2 import FileSystemLoader, ChoiceLoader

template_directories = [
    "themes/business-theme/templates/",
    "mixins/blog/templates/",
    "mixins/photos/templates/",
    "themes/_core/templates/"
]

app.jinja_loader = ChoiceLoader([
    FileSystemLoader(dir) for dir in template_directories
])

When Flask requests base.html, it searches these directories in order. If your theme provides base.html, that wins. If not, it checks mixins. Finally, it falls back to core. Standard Jinja2 behaviour—no custom loaders, no magic, just organised search paths.

The power emerges in how templates reference each other. A blog post template might use:

  • Layout from themes/business/templates/layouts/post.html
  • Components from mixins/blog/templates/components/post-metadata.html
  • Base structure from themes/_core/templates/base.html

Each template extends or includes from the appropriate layer. The theme handles visual presentation. The mixin supplies blog-specific components. Core provides the HTML skeleton. Clean separation, clear responsibilities.

This modularity matters when you're customising. Want a different theme? Swap the theme directory. Need blog functionality? Include the blog mixin. Building something unique? Override just the templates you need whilst inheriting everything else.

For developers coming from other static site generators, this feels familiar but more explicit. Hugo has themes and partials. Jekyll has includes and layouts. MintyFlaskSSG makes the resolution order visible and controllable through standard Flask patterns. You can inspect template_directories, you can add custom loaders, you can debug which template Flask actually loaded.

The separation from MintyFlaskThemes means these patterns live in a reusable package. MintyFlaskSSG provides the loader configuration and template resolution logic. MintyFlaskThemes supplies the actual templates and shows the patterns in practice. Use them together or adapt the patterns elsewhere—they're not coupled to the static site generator.

Photo integration: build-time processing

Photos deserve special attention because they demonstrate MintyFlaskSSG's build-time processing philosophy. The photo system processes images during the build phase, generating multiple sizes (thumbnail, article, gallery) with proper EXIF handling and optimisation.

In Markdown, you reference photos with simple directives:

![Alt text](photo :galleries/vacation/beach.jpg "Sunset at Brighton")

{gallery :galleries/vacation}

During build, the photo processor:

  1. Finds the original image in your photo library
  2. Generates size variants with quality optimisation
  3. Extracts and processes EXIF metadata
  4. Creates URLs pointing to processed images
  5. Builds gallery HTML with PhotoSwipe integration

This happens once, at build time. Your static site contains optimised images and pre-rendered galleries. No JavaScript image manipulation, no client-side processing, no runtime dependencies beyond PhotoSwipe for the lightbox functionality.

The external directory structure keeps source photos separate from your Flask application, whilst processed images live where your static hosting expects them. Clean separation of concerns again.

Why build-time matters

Image processing at build time isn't just about performance—though that matters. It's about predictability and control. When you run flask freeze, you see exactly which images get processed, what sizes are generated, and where they land in your output directory. No surprises, no runtime failures, no "works on my machine" problems.

The external directory structure reflects this philosophy:

project/
├── data/
│   └── photos/
│       ├── photos_original/     # Your source images
│       └── photos_processed/    # Generated variants
└── mintyflask-ssg/
    └── static/                  # Application assets

Source photos live outside your Flask application. The photo processor reads from photos_original/, generates optimised variants, and writes to photos_processed/. Your static site references these processed images. Your source photos remain pristine, unchanged, version-controllable in their original form.

During development, Flask serves processed images through a custom route. During build, Frozen-Flask copies them to your static output. The workflow stays consistent—develop with flask run, build with flask freeze, deploy the result.

Photoswipe integration

For lightbox galleries, MintyFlaskSSG integrates PhotoSwipe rather than building custom JavaScript. This decision matters because it demonstrates the project's approach to third-party dependencies: use established libraries for complex interaction patterns, generate clean HTML that works with them.

PhotoSwipe expects specific HTML structure with data attributes:

<a
  href="/photos/beach.jpg"
  data-pswp-width="1200"
  data-pswp-height="800"
  data-pswp-caption="Sunset at Brighton"
>
  <img src="/photos/beach-thumbnail.jpg" alt="Beach sunset" />
</a>

The photo processor generates this structure during build. When it processes a gallery, it:

  1. Scans the gallery directory for images
  2. Processes each image to multiple sizes
  3. Extracts EXIF metadata for captions and dimensions
  4. Generates HTML with proper PhotoSwipe attributes
  5. Outputs a complete gallery component

Templates receive this pre-built HTML. No template logic for image sizing, no conditional rendering, no runtime EXIF reading. The template includes the gallery component, PhotoSwipe handles interaction, and everything works without complex Jinja2 gymnastics.

This separation keeps templates simple. A gallery template looks like:

<div class="photo-gallery">
    {{ gallery.html | safe }}
</div>

That's it. The complexity lives in the photo processor, which runs once at build time and generates clean, standards-compliant HTML. Templates remain readable and maintainable.

Exif handling and privacy

The photo system's EXIF handling demonstrates thoughtful defaults with configuration options. By default, it:

  • Extracts camera information (make, model, lens)
  • Reads capture settings (aperture, shutter speed, ISO)
  • Processes date/time information
  • Removes GPS coordinates for privacy

This last point matters. Many cameras embed GPS coordinates in photos. Publishing these exposes where you live, work, and travel. MintyFlaskSSG strips GPS data by default whilst preserving useful metadata like camera settings and capture dates.

You can override this in configuration:

PHOTOS_EXIF_REMOVE_GPS = False  # Keep GPS data
PHOTOS_EXIF_COPYRIGHT = 'CC-BY-SA-4.0'  # Add copyright
PHOTOS_EXIF_COPYRIGHT_AUTHOR = 'Your Name'

But the safe default protects privacy automatically. This feels right—handle the security concern by default, make exposure opt-in.

How it fits together

These features—three-layer templates and build-time photo processing—share a common philosophy: complexity belongs in configuration and build processes, not in templates or runtime code.

Templates resolve through clear layers you can reason about. Photo processing happens once, deterministically, with visible results. EXIF metadata gets handled with privacy-first defaults. Third-party libraries like PhotoSwipe integrate through clean HTML rather than complex abstractions.

The development workflow reflects this:

# Develop with live preview
flask run

# Process photos explicitly
flask photos process

# Build static site
flask freeze

# Deploy static files
rsync -av build/ server:/var/www/

Each step does one thing clearly. Flask's development server lets you preview changes. Photo processing generates optimised images. Freeze builds static HTML. Deployment copies files. No hidden steps, no magic rebuilds, no "clear cache and try again" debugging.

For a developer working with MintyFlaskSSG, this clarity matters. You can inspect what the template loader does. You can examine processed images. You can trace how Frozen-Flask generates URLs. The system remains comprehensible even as you extend it.

Practical implications

What does this mean in practice? If you're building a photography portfolio, you can organise photos in directories, write minimal Markdown for gallery descriptions, and let the photo processor handle everything else. Your source images stay pristine. Your static site gets optimised variants. Your galleries work without JavaScript beyond PhotoSwipe.

If you're building a blog with occasional photos, you reference images with simple Markdown directives. The photo processor generates appropriate sizes. Templates receive clean HTML. The workflow stays simple.

If you're building something more complex—documentation with screenshots, a product site with feature galleries, a multi-language portfolio—the three-layer template system lets you compose functionality without duplication. Mixins provide reusable components. Themes handle presentation. Core supplies structure. You extend what you need, inherit everything else.

This is what "starter kit and foundation" means in practice. Simple sites work immediately. Complex sites build on clear patterns. The architecture doesn't fight you because it follows conventions you already understand if you've worked with Flask and Jinja2.

That's the point—make the common cases easy, make the complex cases possible, and keep the whole system comprehensible to developers who understand Flask's patterns. Features reveal philosophy. These features reveal a preference for clarity, composition, and build-time processing over runtime complexity.