All checks were successful
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>
131 lines
4.3 KiB
Python
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
|