Blueprints
Table of contents
- Introduction
- Creating a blueprint
- Writing routes
- Sending forms with a CSRF token
- Serving a feature’s assets
- Shipping the assets in the package
- See also
Introduction
A blueprint groups a feature’s routes, templates and assets, and Flask registers it at startup. Every feature has exactly one, made with create_blueprint in its __init__.py.
from splent_framework.blueprints.base_blueprint import create_blueprint
notes_bp = create_blueprint(__name__)
Creating a blueprint
create_blueprint(__name__) names the blueprint after the feature, without the splent_feature_ prefix. So splent_io.splent_feature_notes gives notes, and splent_feature_notes_tags gives notes_tags. Every url_for("notes.index") relies on that name.
The blueprint finds its templates in the feature’s templates/ and serves its assets/. Extra arguments, such as url_prefix, go to Flask’s Blueprint.
Writing routes
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from splent_framework.services.service_locator import service_proxy
from splent_io.splent_feature_notes import notes_bp
notes_service = service_proxy("NotesService")
@notes_bp.route("/notes/")
@login_required
def index():
notes = notes_service.get_by_user(current_user.id)
return render_template("notes/index.html", notes=notes)
@notes_bp.route("/notes/create", methods=["GET", "POST"])
@login_required
def create():
if request.method == "POST":
notes_service.create(user_id=current_user.id, title=request.form["title"])
flash("Note created.", "success")
return redirect(url_for("notes.index"))
return render_template("notes/create.html")
Routes live in routes.py and handle HTTP only, such as reading the request, rendering, flashing and redirecting. The logic lives in a service.
Sending forms with a CSRF token
Every POST, PUT, PATCH and DELETE must carry a CSRF token, or it gets 400 before the view runs. The framework protects the whole product, so your route needs nothing. The token goes in the template.
<form method="post" action="{{ url_for('notes.create') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
...
</form>
A Flask-WTF form writes the same field with {{ form.hidden_tag() }}. A fetch sends the token in the X-CSRFToken header.
fetch(url, {
method: "POST",
headers: { "X-CSRFToken": form.csrf_token.value },
body: new FormData(form),
});
Exempting a token-authenticated API
from splent_framework.managers.csrf_manager import exempt
def init_feature(app):
exempt(api_bp, app)
An endpoint that authenticates with a bearer token, not the session cookie, may leave the check. exempt takes a blueprint or a view, from init_feature or as a decorator.
Never exempt a view a signed-in browser can reach.
With WTF_CSRF_ENABLED = False, as in the testing configuration, there is no check and csrf_token() returns an empty string.
Serving a feature’s assets
<link rel="stylesheet" href="{{ url_for('notes.assets', subfolder='css', filename='notes.css') }}">
When the feature has an assets/ folder, its blueprint serves it at /<name>/<subfolder>/<path:filename>, with the endpoint <name>.assets. The subfolder is js, css, dist or vendor, and filename may be a nested path such as leaflet/images/marker.png.
- Any other subfolder answers 404, and a path that leaves the subfolder is refused.
- Files go out as they are on disk, with the type guessed from the extension, so a vendored font or SVG arrives intact.
- The URL carries
?v=<mtime>, and a matching version is cached for a year. - A file missing from the installed package is looked up in the feature’s working copy at the workspace root, with a debug line. In development that serves a bundle you compiled there.
Files that exist only to build the assets always answer 404, in development too.
| Never served | Examples |
|---|---|
| Source maps | *.map |
| Build configs | webpack.config.*, rollup.config.*, vite.config.*, babel.config.*, postcss.config.*, tailwind.config.*, tsconfig*.json |
| Package manifests and lock files | package.json, package-lock.json, yarn.lock, pnpm-lock.yaml |
| Dotfiles and dependencies | Any dotfile, anything under node_modules/ |
Shipping the assets in the package
A production image only has the files the built package carries. feature:create writes a MANIFEST.in that ships the whole assets/ tree, the templates, the translations and the migrations. Keep it, and let feature:sync-template bring it up to date.
Shipping
assets/distalone leaves out every stylesheet inassets/cssand every library inassets/vendor. Development still looks fine, because it reads the files off disk, and production comes up unstyled.
See also
- Services, the layer your routes call.
- Feature anatomy, the files of a feature.
- Rate limits and forms, to guard a public form.