Files
orion/app/templates_config.py
Samir Boulahtit 54247ca4f0
All checks were successful
CI / ruff (push) Successful in 18s
CI / pytest (push) Successful in 2h50m43s
CI / validate (push) Successful in 33s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Successful in 50s
CI / deploy (push) Successful in 1m15s
feat(static-assets): cache-bust JS/CSS via ?v=<commit-sha>, immutable in prod
Adds a `static_v(request, name, path=...)` Jinja helper that appends
?v=<commit-sha> from app.core.build_info, plus a CachedStaticFiles
subclass that serves Cache-Control: public, max-age=31536000, immutable
in production and no-cache in development. Browsers refetch JS/CSS
automatically on every deploy without the user having to hard-reload.

- New: app/core/static_files.py (CachedStaticFiles)
- Updated: app/templates_config.py (static_v helper)
- Updated: main.py (use CachedStaticFiles for *_static mounts)
- Codemod: 143 url_for('*_static', path='*.js'|'*.css') → static_v(...)
  across 123 templates. Images/fonts/JSON locales intentionally
  unchanged (out of scope).
- Arch rule: FE-024 (warning) flags raw url_for on JS/CSS to prevent
  drift. Note: FE-008 was already taken by the number_stepper rule.
- docs/proposals/static-asset-cache-busting.md marked Done.

Closes plan from docs/proposals/static-asset-cache-busting.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:35:59 +02:00

131 lines
4.3 KiB
Python

# app/templates_config.py
"""
Shared Jinja2 templates configuration.
All route modules should import `templates` from here to ensure
consistent globals (like translation function) are available.
Template Loading Strategy:
- Core templates from app/templates/ (highest priority)
- Module templates from app/modules/<module>/templates/ (namespaced)
Module templates should use namespace prefix to avoid collisions:
app/modules/cms/templates/cms/admin/pages.html
-> Rendered as: templates.TemplateResponse("cms/admin/pages.html", ...)
"""
import logging
from datetime import datetime
from pathlib import Path
from fastapi.templating import Jinja2Templates
from jinja2 import ChoiceLoader, FileSystemLoader
from app.utils.i18n import (
DEFAULT_LANGUAGE,
LANGUAGE_FLAGS,
LANGUAGE_NAMES,
SUPPORTED_LANGUAGES,
create_translation_context,
)
logger = logging.getLogger(__name__)
# Core templates directory
TEMPLATES_DIR = Path(__file__).parent / "templates"
MODULES_DIR = Path(__file__).parent / "modules"
def create_template_loaders() -> ChoiceLoader:
"""
Create a ChoiceLoader that searches multiple template directories.
Search order:
1. Core templates (app/templates/) - highest priority
2. Module templates (app/modules/<module>/templates/) - in alphabetical order
Returns:
ChoiceLoader configured with all template directories
"""
loaders = [FileSystemLoader(str(TEMPLATES_DIR))] # Core templates first
# Add module template directories
if MODULES_DIR.exists():
for module_dir in sorted(MODULES_DIR.iterdir()):
if module_dir.is_dir():
templates_path = module_dir / "templates"
if templates_path.exists() and templates_path.is_dir():
loaders.append(FileSystemLoader(str(templates_path)))
logger.debug(f"[Templates] Added module templates: {module_dir.name}")
return ChoiceLoader(loaders)
def get_module_template_dirs() -> list[Path]:
"""
Get list of all module template directories.
Useful for debugging and introspection.
Returns:
List of Path objects for module template directories
"""
dirs = []
if MODULES_DIR.exists():
for module_dir in sorted(MODULES_DIR.iterdir()):
if module_dir.is_dir():
templates_path = module_dir / "templates"
if templates_path.exists() and templates_path.is_dir():
dirs.append(templates_path)
return dirs
# Create shared templates instance with multi-directory loader
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
templates.env.loader = create_template_loaders()
# Add translation function to Jinja2 environment globals
# This makes _() available in all templates AND macros
_default_translator = create_translation_context(DEFAULT_LANGUAGE)
templates.env.globals["_"] = _default_translator
templates.env.globals["t"] = _default_translator # Alias
templates.env.globals["SUPPORTED_LANGUAGES"] = SUPPORTED_LANGUAGES
templates.env.globals["DEFAULT_LANGUAGE"] = DEFAULT_LANGUAGE
templates.env.globals["LANGUAGE_NAMES"] = LANGUAGE_NAMES
templates.env.globals["LANGUAGE_FLAGS"] = LANGUAGE_FLAGS
templates.env.globals["current_year"] = datetime.now().year
# Add build info (version, commit, deployed_at) for sidebar footer
from app.core.build_info import get_build_info
from app.core.config import settings as _settings
templates.env.globals["config"] = {
"version": _settings.version,
**get_build_info(),
}
def _asset_version() -> str:
"""Return a stable version string for cache-busting static assets.
Uses the deploy commit SHA from build_info (written by scripts/deploy.sh,
falls back to git HEAD locally). Same SHA across one deploy → browsers
cache; new deploy flips the SHA → browsers refetch.
"""
info = get_build_info()
return info.get("commit") or "dev"
def static_v(request, name: str, *, path: str) -> str:
"""Versioned `url_for` for static assets.
Usage in templates:
<script src="{{ static_v(request, 'dev_tools_static', path='admin/js/app.js') }}"></script>
Emits `<url>?v=<commit-sha>` so deploys break browser caches automatically.
"""
return f"{request.url_for(name, path=path)}?v={_asset_version()}"
templates.env.globals["static_v"] = static_v