================================ Static Assets and Cache-Breaking ================================ Browsers cache static assets aggressively. A stylesheet served once with a long ``Cache-Control`` lifetime may linger in the browser cache for days or weeks, which is exactly what you want for performance -- right up until you deploy a new version of the file. At that point the browser happily keeps serving the stale copy, and your users see broken layouts, old JavaScript, or missing features until they perform a hard refresh. The classic fix is *cache-breaking* (also called cache-busting): you change the URL of the asset whenever its contents change, so the browser treats it as a brand new resource and re-fetches it. Tet's :mod:`tet.static` module implements this with a per-deploy token embedded into the static view's URL prefix, so you can keep aggressive caching headers and still ship updates that take effect immediately. This page explains how the cache-breaker works, how to wire it into your application, and how to emit cache-broken URLs from your templates. How the cachebreaker works -------------------------- When you include :mod:`tet.static`, a single **cache-breaker token** is generated once, at configuration time, and stored on the Pyramid registry as ``config.registry.cachebreaker``. The token is a timestamp -- the current time in milliseconds since the epoch, zero-padded to twelve digits: .. code-block:: python config.registry.cachebreaker = f"{int(time.time() * 1000):012d}" Because the token is computed once per process startup, it is effectively a **per-deploy** value: every fresh deployment (which restarts the application) gets a new token, and every URL generated during that deploy shares the same token. A typical token looks like ``001718539201234``. The token is woven into the URL *prefix* of your static view rather than appended as a query string. When you register a static view with :func:`add_static_view_with_breaker`, the literal ``{breaker}`` placeholder in the view name is replaced by the current token. So a view registered as ``static/{breaker}`` actually serves assets from ``static/001718539201234/...``. Generating a URL for an asset with Pyramid's standard :meth:`~pyramid.request.Request.static_url` then naturally produces a URL that contains the token, with no extra work in your view code or templates. Handling stale and premature URLs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Embedding the token in the path raises an obvious question: what happens to URLs that were generated by a *previous* deploy and are still cached in a user's browser or referenced by an already-loaded page? :mod:`tet.static` installs a small redirector route alongside the static view to handle these cases gracefully: - **Older token** (the request's breaker is less than the current one): the asset has been re-deployed, so the redirector issues a ``301 Moved Permanently`` to the same asset path under the *current* token. The browser follows the redirect and picks up the fresh asset. - **Newer token** (the request's breaker is greater than the current one): this happens transiently during a rolling deploy, when a page served by a newer instance points at assets that an older instance has not caught up with yet. The redirector returns ``503 Service Unavailable`` with a ``Retry-After: 3`` header so the client retries shortly. - **Matching token but no such asset**: a plain ``404 Not Found``. You do not have to configure any of this -- registering the view with :func:`add_static_view_with_breaker` wires up the redirector automatically. Wiring it into your application ------------------------------- Pull the module in with ``config.include`` and then register your static views using the :func:`add_static_view_with_breaker` directive. The ``include`` call adds two directives to the Configurator: :func:`set_cachebreaker` and :func:`add_static_view_with_breaker`. .. code-block:: python from tet.config import application_factory @application_factory() def main(config): config.include("tet.static") config.add_static_view_with_breaker( name="static/{breaker}", path="myapp:static", ) config.scan() The ``name`` argument **must** contain the literal ``{breaker}`` placeholder; if it is missing, :func:`add_static_view_with_breaker` raises a ``ValueError``. The ``path`` argument is an ordinary Pyramid asset specification pointing at the directory that holds your static files. Any extra keyword arguments are forwarded to the underlying :meth:`~pyramid.config.Configurator.add_static_view`, so you can pass options such as ``cache_max_age``: .. code-block:: python config.add_static_view_with_breaker( name="static/{breaker}", path="myapp:static", cache_max_age=31536000, # one year -- safe, because the URL changes per deploy ) Because each deploy mints a new token and therefore a new URL, you can set a very long ``cache_max_age`` without ever risking a stale asset. That is the whole point of cache-breaking: maximum caching, zero staleness. Overriding the token ~~~~~~~~~~~~~~~~~~~~~ The default timestamp token is convenient, but sometimes you want a deterministic value -- for example, to tie the token to a release tag, a Git commit hash, or a value injected by your CI pipeline so that several processes in a cluster all agree on the same token. Use the :func:`set_cachebreaker` directive **before** registering your static views: .. code-block:: python @application_factory() def main(config): config.include("tet.static") config.set_cachebreaker(config.registry.settings["myapp.release"]) config.add_static_view_with_breaker( name="static/{breaker}", path="myapp:static", ) config.scan() :func:`set_cachebreaker` simply replaces ``config.registry.cachebreaker`` with the value you supply. Since :func:`add_static_view_with_breaker` reads the registry at the moment it builds the URL, the override only takes effect for views registered afterward -- so always set it first. Using ``static_url`` in templates --------------------------------- Once the cache-broken static view is registered, you do not need to think about the token in day-to-day template work. Generate asset URLs the normal Pyramid way, with :meth:`request.static_url`, passing the same asset specification style you used for ``path``. The generated URL automatically includes the current cachebreaker token because it is baked into the view's URL prefix. In a Tonnikala template, use ``$`` interpolation -- the expression resolves the chain of attribute access and the method call as a single unit: .. code-block:: html Logo At render time these expand to URLs such as:: /static/001718539201234/style.css /static/001718539201234/app.js /static/001718539201234/img/logo.png After the next deploy the token changes, every emitted URL changes with it, and browsers fetch the new assets -- while any page still pointing at the old token gets a ``301`` redirect to the current one. You can of course use the same call from Python view code or any other renderer: .. code-block:: python @view_config(renderer="myapp:templates/index.tk") def index(request): return { "stylesheet_url": request.static_url("myapp:static/style.css"), } .. note:: Always go through :meth:`request.static_url` (or its template equivalent) rather than hard-coding the ``/static/...`` path yourself. Hard-coded paths will not contain the cachebreaker token, defeating the whole mechanism, and will break if the token-bearing prefix ever changes. See also -------- - :doc:`templating` -- writing Tonnikala templates and using ``$`` interpolation, including expressions like ``$request.static_url(...)``. - :doc:`configuration` -- the application factory, ``config.include`` and registering directives during configuration.