From 5eca46a4bc5adebfb724a1c8e0e42852232152ef Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 23 Jun 2026 13:44:58 -0700 Subject: [PATCH] Add cache-busted static asset helper (#2804) * Add cache-busted static asset helper Add a static() helper for Datasette, plugin, and mounted static assets that appends content-based hashes, caches hashes in production, and serves matching hashed asset URLs with immutable far-future cache headers. Closes #2800 --- datasette/app.py | 88 +++++++++++----- datasette/handle_exception.py | 1 - datasette/templates/_codemirror.html | 4 +- datasette/templates/api_explorer.html | 2 +- datasette/templates/base.html | 6 +- datasette/templates/database.html | 2 +- datasette/templates/debug_allowed.html | 2 +- datasette/templates/debug_autocomplete.html | 2 +- datasette/templates/debug_check.html | 2 +- datasette/templates/debug_rules.html | 2 +- datasette/templates/patterns.html | 2 +- datasette/templates/row.html | 4 +- datasette/templates/table.html | 10 +- datasette/utils/__init__.py | 11 ++ datasette/utils/asgi.py | 13 ++- docs/changelog.rst | 3 +- docs/contributing.rst | 3 + docs/custom_templates.rst | 39 +++++++ docs/internals.rst | 46 +++++++++ docs/plugin_hooks.rst | 5 +- docs/template_context.rst | 9 -- docs/writing_plugins.rst | 11 +- tests/test_html.py | 44 +++++++- tests/test_internals_datasette.py | 108 ++++++++++++++++++++ tests/test_utils.py | 7 ++ 25 files changed, 361 insertions(+), 65 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 57f893fe..9c9b7de4 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -12,7 +12,6 @@ import dataclasses import datetime import functools import glob -import hashlib import httpx import importlib.metadata import inspect @@ -123,6 +122,7 @@ from .utils import ( parse_metadata, resolve_env_secrets, resolve_routes, + sha256_file, tilde_decode, tilde_encode, to_css_class, @@ -314,7 +314,7 @@ async def favicon(request, send): send, str(FAVICON_PATH), content_type="image/png", - headers={"Cache-Control": "max-age=3600, immutable, public"}, + headers={"Cache-Control": "max-age=3600, public"}, ) @@ -348,6 +348,16 @@ def _legacy_template_csrftoken(context): return "" +def _resolve_static_asset_path(root_path, path): + root = Path(root_path).resolve() + full_path = (root / path).resolve() + try: + full_path.relative_to(root) + except ValueError: + raise ValueError("Static asset path cannot escape static root") from None + return full_path + + # Documentation for the variables Datasette.render_template() adds to the # context for every page. This is part of the documented template contract: # keys added in render_template() must be documented here - the contract @@ -361,9 +371,6 @@ TEMPLATE_BASE_CONTEXT = { "menu_links": "Async function returning links for the Datasette application menu, including links added by plugins. Each item is a link dictionary with ``href`` and ``label`` keys. See :ref:`plugin_hook_menu_links`; for page action menus that can also include JavaScript-backed buttons, see :ref:`plugin_actions`.", "display_actor": "Function that accepts an actor dictionary and returns the display string used in the navigation menu.", "show_logout": "True if the logout link should be shown in the navigation menu", - "app_css_hash": "Hash of Datasette's app.css contents, used for cache busting", - "edit_tools_js_hash": "Hash of Datasette's edit-tools.js contents, used for cache busting", - "table_js_hash": "Hash of Datasette's table.js contents, used for cache busting", "zip": "Python's ``zip()`` builtin, made available to template logic", "body_scripts": 'List of JavaScript snippets contributed by plugins using :ref:`plugin_hook_extra_body_script`. Each item is a dictionary with ``script`` containing JavaScript source and ``module`` indicating whether Datasette will wrap it in `` - + + diff --git a/datasette/templates/row.html b/datasette/templates/row.html index 5e483d34..aa1f0ecd 100644 --- a/datasette/templates/row.html +++ b/datasette/templates/row.html @@ -7,9 +7,9 @@ {% if row_mutation_ui %} {% if table_page_data.foreignKeys %} - + {% endif %} - + {% endif %}