Practical guide to working with MintyFlask's template resolution system - common scenarios, patterns, and troubleshooting

Template Resolution User Guide

Overview

This guide covers practical scenarios you'll encounter when working with MintyFlask templates. It assumes you've read the Quick Start and are comfortable with the two basic patterns.

What you'll learn:

  • Working with the three template resolution functions
  • Common scenarios and when to use each pattern
  • Configuring layouts vs overriding blocks
  • Troubleshooting template resolution issues
  • Best practices for template organisation

The Three Resolution Functions

MintyFlask provides three ways to resolve templates, each for different situations.

1. resolve_base_template() - Standard Resolution

When to use: Extending core layouts (base.html, single.html, default.html, etc.)

Syntax:

{% extends resolve_base_template('template_name.html') %}

Resolution path:

  1. Look in current theme: themes/blog/templates/template_name.html
  2. Fall back to core: themes/_core/templates/template_name.html

Example - Extending base layout:

{# themes/blog/templates/pages/services.html #}
{% extends resolve_base_template('base.html') %}

{% block content %}
<div class="services-page">
    <h1>Our Services</h1>
    {# Page content #}
</div>
{% endblock %}

Example - Extending single content layout:

{# themes/blog/templates/pages/post-single.html #}
{% extends resolve_base_template('layouts/single.html') %}

{# Customise the article class #}
{% block article_class %}single-content blog-post featured{% endblock %}

2. Simple extends - Search Path Resolution

When to use: Extending mixin layouts, including partials, importing macros

Syntax:

{% extends "template_name.html" %}
{% include "partial_name.html" %}
{% from "macros.html" import macro_name %}

Resolution path: Flask searches in order:

  1. themes/blog/templates/
  2. mixins/blog/templates/
  3. themes/_core/templates/

Example - Page extending mixin layout:

{# themes/blog/templates/pages/category.html #}
{% extends "layouts/post-list.html" %}

{# Configure the layout through variables #}
{% set page_title = "Category: " + category %}
{% set page_description = "Articles in this category" %}
{% set pagination_endpoint = 'blog_bp.category_view' %}
{% set pagination_params = {'tag': tag} %}

Example - Including partials:

{# themes/blog/templates/pages/archive.html #}
{% extends resolve_base_template('base.html') %}

{% block content %}
<div class="archive-page">
    <h1>Archives</h1>

    {% for post in posts %}
        {# Include reusable post card partial #}
        {% include "partials/post-card.html" %}
    {% endfor %}
</div>
{% endblock %}

Example - Importing macros:

{# themes/blog/templates/pages/blog-index.html #}
{% extends resolve_base_template('base.html') %}
{% from "blog-components.html" import blog_post_list with context %}

{% block content %}
<div class="blog-index">
    <h1>Recent Articles</h1>

    {# Use macro to render post list #}
    {{ blog_post_list(posts, layout='grid', columns=3) }}
</div>
{% endblock %}

3. resolve_template_with_fallback() - Multi-Level Resolution

When to use: Complex theme hierarchies with multiple inheritance levels (advanced use case)

Syntax:

{% extends resolve_template_with_fallback(
    'template_name.html',
    fallback_themes=['parent_theme', 'grandparent_theme']
) %}

Resolution path: Current theme → Fallback 1 → Fallback 2 → Core

Example - Theme variant system:

{# themes/blog-professional/templates/base.html #}
{#
 # This professional variant inherits from minimal theme,
 # which inherits from core
 #}
{% extends resolve_template_with_fallback(
    'base.html',
    fallback_themes=['minimal']
) %}

{% block extra_head %}
    {{ super() }}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/professional.css') }}">
{% endblock %}

Note: Most projects won't need this function. Use it only when building theme variant systems or complex theme hierarchies.

Common Scenarios

Scenario 1: Creating a New Static Page

Goal: Add an "About" page to your site.

Steps:

  1. Create template file: themes/blog/templates/pages/about.html
  2. Extend base layout with resolve_base_template()
  3. Add your content in blocks
  4. Create Flask route to render it

Template:

{# themes/blog/templates/pages/about.html #}
{% extends resolve_base_template('base.html') %}

{% block content %}
<article class="prose prose-lg dark:prose-invert mx-auto">
    <h1>About MintyFlask</h1>

    <p>
        MintyFlask is a flexible theming system built with Flask
        and Tailwind CSS. It provides semantic CSS components and
        a powerful template resolution system.
    </p>

    <h2>Our Mission</h2>
    <p>
        To make web development more accessible whilst maintaining
        professional standards and best practices.
    </p>
</article>
{% endblock %}

Flask route:

@main_bp.route('/about')
def about():
    return render_template('pages/about.html')

Scenario 2: Customising a Mixin Layout

Goal: Create a category page that uses the blog mixin's post-list layout.

Steps:

  1. Create page template: themes/blog/templates/pages/category.html
  2. Extend mixin layout with simple extends
  3. Configure layout through variables
  4. Optionally override specific blocks

Template:

{# themes/blog/templates/pages/category.html #}
{% extends "layouts/post-list.html" %}

{# Configure page-specific variables #}
{% set page_title = "Category: " + category %}
{% set page_description = "Articles in this category" %}

{# Pagination configuration #}
{% set pagination_endpoint = 'blog_bp.category_view' %}
{% set pagination_params = {'category': category} %}

{# Display configuration #}
{% set show_related_topics = true %}
{% set related_topics_config = {
    'title': 'Explore Related Content',
    'links_config': [
        {
            'url': url_for('blog_bp.categories'),
            'text': 'All Categories',
            'icon': 'M19 11H5m14...'
        }
    ]
} %}

{# Optional: Override empty state if needed #}
{% block empty_state_actions %}
<div class="card-actions card-actions-center">
    <a href="{{ url_for('blog_bp.categories') }}"
       class="btn-base btn-intent-secondary btn-size-sm">
        Browse All Categories
    </a>
</div>
{% endblock %}

Flask route:

@blog_bp.route('/category/<category>')
def category_view(category):
    posts = get_posts_by_category(category)
    return render_template('pages/category.html',
                          category=category,
                          posts=posts)

Scenario 3: Creating a Custom Layout

Goal: Create a two-column layout for a landing page.

Steps:

  1. Create layout: themes/blog/templates/layouts/landing.html
  2. Extend base with resolve_base_template()
  3. Define content blocks for customisation
  4. Create page that uses your layout

Layout:

{# themes/blog/templates/layouts/landing.html #}
{% extends resolve_base_template('base.html') %}

{% block content %}
<div class="landing-layout">
    {# Hero section #}
    <section class="hero bg-gradient-to-r from-blue-500 to-purple-600 py-20">
        {% block hero %}
        <div class="container mx-auto text-centre text-white">
            <h1 class="text-5xl font-bold mb-4">
                {% block hero_title %}Welcome{% endblock %}
            </h1>
            <p class="text-xl mb-8">
                {% block hero_subtitle %}Get started today{% endblock %}
            </p>
        </div>
        {% endblock %}
    </section>

    {# Two-column content #}
    <div class="container mx-auto py-12">
        <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
            <div class="main-content">
                {% block main_content %}
                <h2>Main Content</h2>
                <p>Your primary content here...</p>
                {% endblock %}
            </div>

            <div class="sidebar">
                {% block sidebar %}
                <h3>Sidebar</h3>
                <p>Additional information...</p>
                {% endblock %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

Page using the layout:

{# themes/blog/templates/pages/landing.html #}
{% extends "layouts/landing.html" %}

{% block hero_title %}Transform Your Workflow{% endblock %}
{% block hero_subtitle %}Powerful tools for modern development{% endblock %}

{% block main_content %}
<h2>Key Features</h2>
<ul>
    <li>Flexible theming system</li>
    <li>Semantic CSS components</li>
    <li>Dark mode support</li>
</ul>
{% endblock %}

{% block sidebar %}
<div class="card-base card-intent-feature">
    <div class="card-body">
        <h3 class="card-title">Get Started</h3>
        <p class="card-description">Ready to begin?</p>
        <a href="/docs" class="btn-base btn-intent-primary">
            Read the Docs
        </a>
    </div>
</div>
{% endblock %}

Scenario 4: Using Multiple Macros

Goal: Build a complex page using several reusable components.

Template:

{# themes/blog/templates/pages/blog-home.html #}
{% extends resolve_base_template('base.html') %}

{# Import multiple macros - note 'with context' #}
{% from "blog-components.html" import
   blog_post_list,
   post_metadata_enhanced,
   post_tag_cloud
   with context %}

{% from "components/pagination.html" import render_pagination with context %}

{% block content %}
<div class="blog-home">
    {# Hero section #}
    <section class="hero mb-12">
        <h1 class="text-4xl font-bold">Latest Articles</h1>
        <p class="text-xl text-slate-600 dark:text-stone-400">
            Insights on web development and design
        </p>
    </section>

    {# Featured post #}
    {% if posts|length > 0 %}
    <article class="featured-post mb-12">
        {% set featured = posts[0] %}
        <h2 class="text-3xl font-bold mb-4">
            <a href="{{ url_for('blog_bp.post_view', slug=featured.slug) }}">
                {{ featured.meta.title }}
            </a>
        </h2>

        {# Use metadata macro #}
        {{ post_metadata_enhanced(featured, show_reading_time=true) }}

        <p class="text-lg mt-4">{{ featured.meta.summary }}</p>

        {# Use tag cloud macro #}
        {{ post_tag_cloud(featured) }}
    </article>
    {% endif %}

    {# Recent posts grid #}
    <section class="recent-posts">
        <h2 class="text-2xl font-bold mb-6">Recent Posts</h2>

        {# Use post list macro #}
        {{ blog_post_list(posts[1:5], layout='grid', columns=2) }}
    </section>

    {# Pagination #}
    {% if pagination %}
        {{ render_pagination(pagination, endpoint='blog_bp.blog_index') }}
    {% endif %}
</div>
{% endblock %}

Scenario 5: Overriding a Core Template

Goal: Customise the main base.html layout for your theme.

Note: This is less common - usually you configure rather than override. But sometimes you need to change the fundamental structure.

Steps:

  1. Create themes/blog/templates/base.html
  2. Extend the core base with resolve_base_template()
  3. Override specific blocks
  4. Use {{ super() }} to preserve core functionality

Template:

{# themes/blog/templates/base.html #}
{#
 # Custom base layout that extends core base
 # Adds site-wide banner and custom footer
 #}
{% extends resolve_base_template('layouts/default.html') %}

{# Add custom CSS #}
{% block extra_head %}
    {{ super() }}
    <link rel="stylesheet" href="{{ theme_static('css/custom.css') }}">
{% endblock %}

{# Override main content wrapper to add banner #}
{% block main %}
    {# Site-wide announcement banner #}
    <div class="announcement-banner bg-blue-600 text-white py-2 text-centre">
        <p>🎉 New features released! <a href="/changelog" class="underline">Learn more</a></p>
    </div>

    {# Preserve core main block #}
    {{ super() }}
{% endblock %}

{# Custom footer #}
{% block footer %}
    <footer class="site-footer bg-slate-900 text-white py-12">
        <div class="container mx-auto">
            <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
                <div>
                    <h3 class="font-bold mb-4">About</h3>
                    <p>{{ config.SITE_DESCRIPTION }}</p>
                </div>
                <div>
                    <h3 class="font-bold mb-4">Links</h3>
                    <ul>
                        <li><a href="/about">About</a></li>
                        <li><a href="/contact">Contact</a></li>
                        <li><a href="/privacy">Privacy</a></li>
                    </ul>
                </div>
                <div>
                    <h3 class="font-bold mb-4">Social</h3>
                    {# Social links #}
                </div>
            </div>
        </div>
    </footer>
{% endblock %}

Configuration vs Block Overrides

Prefer configuration over block overrides. Most mixin layouts accept configuration variables that let you customise behaviour without overriding blocks.

Good: Configuration Approach

{# themes/blog/templates/pages/tag.html #}
{% extends "layouts/post-list.html" %}

{# Configure the layout #}
{% set page_title = "Tag: " + tag %}
{% set show_related_topics = true %}
{% set pagination_endpoint = 'blog_bp.tag_view' %}
{% set pagination_params = {'tag': tag} %}

Benefits:

  • Cleaner, more maintainable
  • Less code duplication
  • Easier to update when layout changes
  • Clear separation of concerns

Less Ideal: Block Override Approach

{# themes/blog/templates/pages/tag.html #}
{% extends "layouts/post-list.html" %}

{# Override blocks #}
{% block page_header %}
<header>
    <h1>Tag: {{ tag }}</h1>
    <p>Articles with this tag</p>
    {# Lots of custom HTML #}
</header>
{% endblock %}

Drawbacks:

  • More code to maintain
  • Duplicates layout logic
  • May break when layout updates
  • Harder to track customisations

When to override blocks:

  • The layout doesn't support your use case
  • You need fundamentally different structure
  • You're creating a completely custom section

Troubleshooting

Template Not Found

Error:

jinja2.exceptions.TemplateNotFound: layouts/post-list.html

Possible causes:

  1. Wrong resolution function - Using resolve_base_template() for a mixin template
  2. Typo in filename - Check spelling and path
  3. Template not in search path - Verify file location

Solutions:

{# If extending mixin layout, use simple extends #}
{% extends "layouts/post-list.html" %}

{# If extending core layout, use resolve_base_template #}
{% extends resolve_base_template('layouts/single.html') %}

{# Verify file exists at correct path #}
{# mixins/blog/templates/layouts/post-list.html #}

Import Context Issues

Error:

jinja2.exceptions.UndefinedError: 'url_for' is undefined

Cause: Missing with context when importing macros that use Flask functions.

Solution:

{# Wrong #}
{% from "blog-components.html" import blog_post_list %}

{# Correct #}
{% from "blog-components.html" import blog_post_list with context %}

When to use with context:

  • Macro uses url_for()
  • Macro uses config
  • Macro uses request
  • Macro uses any Flask/Jinja2 globals

When NOT to use with context:

  • Pure formatting functions
  • Macros that only manipulate passed data
  • Simple rendering helpers

Wrong Template Resolved

Symptom: Getting unexpected template or layout.

Cause: Name collision or wrong resolution path.

Debug steps:

  1. Check template name - Are multiple templates named the same?
  2. Verify resolution function - Using correct function for the layer?
  3. Check search path order - Which directory comes first?

Solution:

{# Use unique names across mixins #}
✗ mixins/blog/templates/layouts/list.html
✗ mixins/docs/templates/layouts/list.html

✓ mixins/blog/templates/layouts/post-list.html
✓ mixins/docs/templates/layouts/docs-list.html

Circular Inheritance

Error:

RecursionError: maximum recursion depth exceeded

Cause: Template trying to extend itself.

Example problem:

{# themes/blog/templates/base.html #}
{% extends resolve_base_template('base.html') %}
{# This tries to extend itself! #}

Solution:

{# themes/blog/templates/base.html #}
{% extends resolve_base_template('layouts/default.html') %}
{# Extend a different template #}

Best Practices

1. Use Descriptive Template Names

✓ Good Names:
- post-list.html (clear purpose)
- blog-home.html (specific context)
- post-archive.html (describes content)

✗ Avoid:
- list.html (too generic)
- page.html (ambiguous)
- template.html (meaningless)

2. Document Template Dependencies

Add comments explaining template relationships:

{# themes/blog/templates/pages/category.html #}
{#
 # Category listing page
 #
 # Extends: layouts/post-list.html (blog mixin)
 # Requires: category, posts variables from route
 # Configures: pagination, related topics, statistics
 #}
{% extends "layouts/post-list.html" %}

3. Keep Template Hierarchy Shallow

✓ Good: 2-3 levels
pages/category.html → layouts/post-list.html → base.html

✗ Avoid: Deep hierarchies
pages/category.html → layouts/blog-list.html →
layouts/generic-list.html → layouts/base-list.html → base.html

4. Test Template Resolution

Verify templates resolve as expected:

# In Flask shell or test
from flask import render_template

try:
    result = render_template('pages/category.html',
                            category='Technology',
                            posts=[])
    print("✓ Template resolved successfully")
except Exception as e:
    print(f"✗ Resolution failed: {e}")

5. Organise by Purpose

themes/blog/templates/
├── pages/              # URL endpoint templates
│   ├── blog-index.html
│   ├── category.html
│   └── post-single.html
├── layouts/            # Theme-specific layouts (rare)
│   └── custom-landing.html
└── components/         # Theme-specific components (rare)

mixins/blog/templates/
├── layouts/            # Reusable layouts (common)
│   ├── post-list.html
│   └── blog-home.html
├── partials/           # Reusable partials (common)
│   ├── post-card.html
│   └── post-meta.html
└── blog-components.html # Central macros

Quick Reference

When to Use Each Pattern

Context Function Example
Extend core base resolve_base_template() {% extends resolve_base_template('base.html') %}
Extend core layout resolve_base_template() {% extends resolve_base_template('layouts/single.html') %}
Extend mixin layout Simple extends {% extends "layouts/post-list.html" %}
Include partial Simple include {% include "partials/post-card.html" %}
Import macro (uses Flask) from...import with context {% from "..." import macro with context %}
Import macro (pure) from...import {% from "..." import format_date %}
Complex theme hierarchy resolve_template_with_fallback() Advanced - see reference docs

Common Mistakes to Avoid

Mistake Correct Approach
{% extends "base.html" %} (for core) {% extends resolve_base_template('base.html') %}
{% extends resolve_base_template('layouts/post-list.html') %} {% extends "layouts/post-list.html" %}
Import without context (using url_for) Add with context
Duplicate template names across mixins Use unique, descriptive names
Deep template hierarchies (5+ levels) Keep it shallow (2-3 levels)

Next Steps

You now understand how to work with MintyFlask's template resolution system in practical scenarios. For more information:

Summary

Key takeaways:

  1. Choose the right function:

    • resolve_base_template() for core layouts
    • Simple extends for mixin layouts
    • resolve_template_with_fallback() for advanced hierarchies
  2. Prefer configuration over block overrides - it's cleaner and more maintainable

  3. Use with context when macros need Flask functions

  4. Keep template names unique and descriptive

  5. Document your template dependencies

  6. Test your template resolution paths

With these patterns and practices, you can build flexible, maintainable template structures that leverage MintyFlask's layered architecture effectively.