Flask blueprints are great for splitting a growing app into manageable modules—until two blueprints both define a index.html and Flask quietly serves the wrong one. This post walks through every root cause of blueprint template conflicts and shows you exactly how to fix each one.

Quick Answer

Prefix each blueprint’s templates with a subfolder that matches the blueprint name:

templates/
  admin/
    index.html
  dashboard/
    index.html

Then call render_template("admin/index.html") instead of render_template("index.html"). For blueprint-local templates, pass template_folder="templates" when you register the blueprint and use the same subfolder convention inside it.


How Flask resolves templates

Before diving into fixes, it helps to know the lookup order. When you call render_template("index.html"), Flask searches:

  1. The app-level templates/ folder (relative to the application root)
  2. Each blueprint’s template_folder (if one was specified), in the order the blueprints were registered

The first match wins. That’s the entire source of every template conflict: Flask finds a file earlier in the search order than you expected.


Diagnostic steps

Run this snippet in a Flask shell (flask shell) to see exactly which template file gets resolved:

from flask import current_app

# List every template loader and its search path
for loader in current_app.jinja_env.loader.loaders:
    print(type(loader).__name__, getattr(loader, 'searchpath', '(no searchpath)'))

You’ll see something like:

FileSystemLoader ['/home/user/myapp/templates']
FileSystemLoader ['/home/user/myapp/admin/templates']
FileSystemLoader ['/home/user/myapp/dashboard/templates']

That order is your conflict map. Any file that exists in more than one path will always resolve to the first loader that contains it.


Cause #1: Duplicate template names across blueprints

This is by far the most common issue. You have two blueprints, each with a sensibly named index.html, and Flask always renders one blueprint’s version even when the other blueprint handles the request.

The problematic structure:

myapp/
  admin/
    __init__.py
    templates/
      index.html      ← admin landing page
  dashboard/
    __init__.py
    templates/
      index.html      ← dashboard landing page
  app.py
# admin/__init__.py
from flask import Blueprint, render_template

admin_bp = Blueprint("admin", __name__, template_folder="templates")

@admin_bp.route("/admin/")
def index():
    return render_template("index.html")   # Which index.html?!

If admin was registered before dashboard, every render_template("index.html") call—regardless of which blueprint is handling the request—resolves to admin/templates/index.html.

The fix — namespace your templates with a subdirectory:

myapp/
  admin/
    templates/
      admin/          ← subdirectory matches blueprint name
        index.html
  dashboard/
    templates/
      dashboard/
        index.html
# admin/__init__.py
@admin_bp.route("/admin/")
def index():
    return render_template("admin/index.html")   # unambiguous
# dashboard/__init__.py
@dashboard_bp.route("/dashboard/")
def index():
    return render_template("dashboard/index.html")

This one change eliminates the entire class of collision. It’s also the convention used by Flask’s own documentation and most large open-source Flask projects.


Cause #2: Missing template_folder on the blueprint

If you don’t pass template_folder when creating a blueprint, Flask won’t add that blueprint’s local template directory to the search path at all. Every render_template call then falls through to the app-level templates/ folder, which may not have the file you expect—or may have a stale copy from a different blueprint.

Broken registration:

# admin/__init__.py
from flask import Blueprint

# No template_folder → Flask never looks in admin/templates/
admin_bp = Blueprint("admin", __name__)
TemplateNotFound: admin/index.html

Fixed registration:

# admin/__init__.py
from flask import Blueprint

admin_bp = Blueprint(
    "admin",
    __name__,
    template_folder="templates",   # now Flask knows where to look
    static_folder="static",        # optional, but keep it consistent
)

The template_folder path is relative to the blueprint’s package directory (i.e., relative to the file where the Blueprint(...) call lives). So if your blueprint is at myapp/admin/__init__.py, then template_folder="templates" resolves to myapp/admin/templates/.

Quick sanity check — after adding template_folder, re-run the loader inspection snippet from the Diagnostic steps section. You should see the blueprint’s folder appear in the list.


Cause #3: App-level template shadows blueprint template

Sometimes you centralize all templates in one app-level folder for simplicity, but then a template intended for a specific blueprint gets overridden by a file with the same name in the app folder.

myapp/
  templates/
    base.html
    index.html        ← unintended catch-all
  admin/
    templates/
      admin/
        index.html    ← never reached because app-level wins first

Because the app-level templates/ is always searched first, render_template("admin/index.html") would only match the app-level file if one exists there—your blueprint-local file is invisible.

Option A: Consolidate into app-level templates (simpler apps)

Just put everything in one templates/ folder and skip template_folder on blueprints entirely. Use subdirectories to namespace:

templates/
  admin/
    index.html
    users.html
  dashboard/
    index.html
    widgets.html
  base.html

This is the right call for smaller apps where you don’t need blueprint portability.

Option B: Keep templates inside each blueprint (modular apps)

Remove the duplicate from the app-level folder and rely solely on blueprint-local templates. Only put truly shared templates (base.html, error pages, shared components) in the app-level folder:

myapp/
  templates/
    base.html         ← shared layout only
    404.html
  admin/
    templates/
      admin/
        index.html    ← blueprint-specific
  dashboard/
    templates/
      dashboard/
        index.html

The key rule: don’t put the same filename in both the app-level and blueprint-level folders unless you deliberately want the app-level version to take precedence.


Cause #4: Blueprint registration order changes template priority

Flask’s template loader respects registration order. If you register blueprints in different orders across environments (e.g., dev vs. production, or depending on a feature flag), the “winning” template can change unpredictably.

# This order matters more than most developers realize
app.register_blueprint(admin_bp)      # admin templates searched second
app.register_blueprint(dashboard_bp)  # dashboard templates searched third

If both admin/templates/ and dashboard/templates/ contain shared/header.html, then render_template("shared/header.html") always picks the admin version because admin_bp was registered first.

The fix: don’t rely on registration order for disambiguation. Use the subfolder convention (Cause #1 fix) so each template name is unique regardless of search order. If you have genuinely shared templates, put them in the app-level templates/ folder with a shared/ subdirectory.


Cause #5: Template inheritance breaks across blueprint boundaries

This one shows up when you use {% extends %}. If your blueprint’s index.html tries to extend base.html, Flask has to resolve base.html through the same loader chain. If base.html lives in the app-level templates/ folder and the blueprint’s loader runs first, Flask won’t find it.

The broken extend:

{# admin/templates/admin/index.html #}
{% extends "base.html" %}   {# Flask searches admin/templates first, no base.html there #}
TemplateNotFound: base.html

Fix option 1 — put shared base templates in app-level templates/:

Move base.html to myapp/templates/base.html. Since the app-level loader always runs before blueprint loaders, this works regardless of which blueprint is rendering.

Fix option 2 — use a full path if base lives inside a blueprint:

{% extends "admin/base.html" %}

This tells Flask to look for admin/base.html everywhere, which will match the namespaced copy inside admin/templates/admin/base.html.

The rule of thumb: base templates and shared layouts belong in the app-level templates/ folder. Blueprint-local templates extend them, not the other way around.


Cause #6: Incorrect url_for calls for blueprint static files

This isn’t a render_template issue, but it causes a very similar-looking error in templates—BuildError instead of TemplateNotFound. When templates reference static files using url_for("static", filename="..."), they’re pointing at the app-level static folder. Blueprint-specific static files need the blueprint name prefix.

{# Broken — looks in app-level static/ #}
<link rel="stylesheet" href="{{ url_for('static', filename='admin/style.css') }}">

{# Fixed — looks in admin blueprint's static/ #}
<link rel="stylesheet" href="{{ url_for('admin.static', filename='style.css') }}">

For this to work, the blueprint also needs static_folder configured:

admin_bp = Blueprint(
    "admin",
    __name__,
    template_folder="templates",
    static_folder="static",
    static_url_path="/admin/static",  # avoids collision with app-level /static
)

Still not working?

A few less-common situations:

Cached templates in production: If you’re running with TEMPLATES_AUTO_RELOAD = False (the default in production), Flask caches compiled templates. After fixing a conflict, you may need to restart the server to clear the cache. In development, set TEMPLATES_AUTO_RELOAD = True in your config.

__init__.py vs module files: The __name__ you pass to Blueprint() determines how Flask computes the package root for template_folder. If your blueprint is defined in admin/views.py rather than admin/__init__.py, __name__ is myapp.admin.views and the template folder resolves relative to admin/views.py—which is still admin/, so it works fine. But if you move the blueprint definition to a different subpackage, double-check the path resolves where you think it does.

Installed packages with templates: If you install a third-party Flask extension that ships its own templates (Flask-Admin, for example), those templates go into the extension’s package directory. They can collide with your app templates if you use identical names. The safest fix is to override extension templates by placing your version at the exact same relative path inside your app’s templates/ folder—since app-level templates win, Flask will use yours.

Jinja2 environment shared across blueprints: All blueprints share the same app.jinja_env. Custom filters, globals, or tests you define on the Jinja2 environment are visible to every template regardless of which blueprint rendered it. This is usually desirable, but if two blueprints define the same custom filter under different names, the last registration wins. Keep filter/global definitions in app.py or a dedicated extensions.py to avoid surprises.


Summary checklist

Use Debugly’s trace formatter to quickly parse and analyze Python tracebacks from TemplateNotFound and related Flask errors—paste the full traceback to pinpoint exactly which loader and path was searched.

  • [ ] Each blueprint’s templates live inside a subdirectory named after the blueprint (admin/templates/admin/)
  • [ ] render_template calls use the full namespaced path ("admin/index.html", not "index.html")
  • [ ] Every blueprint that has its own templates was created with template_folder="templates"
  • [ ] Shared/base templates are in the app-level templates/ folder, not inside any one blueprint
  • [ ] Blueprint static files use url_for("blueprint_name.static", ...) not url_for("static", ...)
  • [ ] Static URL paths are unique per blueprint (static_url_path="/admin/static")
  • [ ] Production server was restarted after fixing template paths (clears Jinja2 cache)
  • [ ] Used the loader inspection snippet to verify the actual search order

Related reading

If you’re running into broader Flask issues, check out our guides on Flask SQLAlchemy session management and Flask request context errors. Both cover problems that often surface alongside blueprint refactors.

The moment you adopt the “blueprint subfolder” naming convention consistently across your project, an entire category of template bugs disappears. It’s one of those changes that feels like a small refactor but pays dividends every time you add a new blueprint.