When Django's ManifestStaticFilesStorage post-processes CSS, how are url() and @import references rewritten, and why can that break collectstatic?
answer
- stylesheets point at other files
- rewrite before hashing yourself
- a changed image renames the CSS
- source-map comments count too
- five passes, then give up
basics
~20 sDuring collectstatic, ManifestStaticFilesStorage rewrites url(), @import and source-map references in CSS to the targets' hashed names before hashing the CSS itself; a reference to a file that was not collected raises ValueError and aborts the command.
solid answer
~40 s`post_process()` scans every `*.css` file with regex patterns for `url(...)`, `@import` and `/*# sourceMappingURL=... */`, plus `//# sourceMappingURL` lines in `*.js`. Each relative reference is resolved against the stylesheet's own directory (absolute ones must start with `STATIC_URL`), looked up in the hashed names being built, and replaced, so `../img/hero.png` becomes `../img/hero.acae32e4532b.png`. Only then is the CSS file hashed, so changing an image gives the stylesheet a new name too. URLs with a scheme (`https:`, `data:`), protocol-relative `//` URLs and absolute paths outside `STATIC_URL` are left alone. If a target does not exist, `hashed_name()` raises `ValueError` ("The file ... could not be found"), which `collectstatic` re-raises as a `CommandError` naming the referencing file and line. Vendored CSS pointing at a font or `.map` file you did not ship is the classic trigger.
code
css · 7 lines/* static/css/site.css as written */
@import 'reset.css';
.hero { background: url('../img/hero.png'); }
/* staticfiles/css/site.<hash>.css after collectstatic */
@import url("reset.8b1a9953c461.css");
.hero { background: url("../img/hero.acae32e4532b.png"); }go deeper
Recall that stylesheets referencing images and fonts are rewritten during collectstatic so they point at the hashed names.
Explain which references the patterns match, how relative paths are resolved, and why the stylesheet is hashed only after its references are rewritten.
Diagnose a failing collectstatic from the ValueError note, and choose between shipping the file, fixing the path and narrowing the patterns for vendored assets.
Decide where asset rewriting belongs when a front-end bundler also rewrites URLs, so references are rewritten once and the build fails loudly on a broken link.
## Why CSS needs rewriting at all Hashing file names only busts caches if everything that **points at** a file points at the new name. Templates get new names through the manifest lookup, but stylesheets reference images, fonts and other stylesheets directly. If `css/site.css` kept saying `url('../img/hero.png')` after the image was renamed to a hashed copy, the browser would load the unhashed original, and a cached copy of it could survive the deploy. So `ManifestStaticFilesStorage` rewrites those references inside the files it collects. ## What the patterns match The rewriting rules live in `HashedFilesMixin.patterns`: - In `*.css`: `url(...)` with or without quotes. - In `*.css`: `@import 'x.css'`, rewritten to the form `@import url("x.<hash>.css")`. - In `*.css`: `/*# sourceMappingURL=... */` comments. - In `*.js`: `//# sourceMappingURL=...` lines. - JavaScript `import`/`export ... from` statements only when a subclass sets `support_js_module_import_aggregation = True`, which the docs call experimental. ## What is left untouched - URLs with a scheme, such as `https:` or `data:` URIs. - Protocol-relative URLs starting with `//`. - Absolute paths that do not start with `STATIC_URL` (they point at something Django does not collect). - References inside ordinary CSS comments, which Django 6.1's patterns skip (a `sourceMappingURL` comment is the deliberate exception, matched by its own pattern). - Query strings and fragments are preserved around the rewritten name, so `font.eot?#iefix` keeps its suffix. ## How one reference is resolved 1. A relative URL is joined to the directory of the stylesheet being processed; an absolute one has `STATIC_URL` stripped. 2. The resulting path is looked up among the hashed names produced so far, or hashed from disk. 3. Only the final path segment is replaced, so `../img/hero.png` becomes `../img/hero.acae32e4532b.png` and stays relative. 4. The rewritten stylesheet is saved, and its own hash is computed from the **rewritten** bytes. ## Cascading hashes and repeated passes Because a stylesheet is hashed after its references are rewritten, a change deep in the graph propagates upward. Replace `img/hero.png`, rerun `collectstatic`, and `css/site.css` gets a new hashed name even though nobody edited it, so the HTML naming it changes too. That is the behaviour you want after a deploy: one image change is enough to push new CSS to every visitor. Stylesheets can reference other stylesheets, so one pass may not settle every name. `post_process()` repeats the rewrite until no hash changes, up to `max_post_process_passes`, which defaults to **5**. Circular references (`foo.css` imports `bar.css` which imports `foo.css`) never converge, and `collectstatic` fails with `RuntimeError("Max post-process passes exceeded.")`. A subclass can raise the limit for deep but acyclic graphs. ## When it breaks collectstatic If a referenced target cannot be found, `hashed_name()` raises `ValueError("The file 'fonts/icons.woff2' could not be found with <...ManifestStaticFilesStorage...>.")`. Django attaches a note naming the stylesheet and the line containing the reference, and `collectstatic` re-raises the error as a `CommandError`. The new `staticfiles.json` is written only after post-processing completes, so a failed run does not produce a new manifest. Typical triggers: - A vendored theme whose CSS references fonts or images you did not copy. - A minified library ending in a `sourceMappingURL` comment for a `.map` file that was not shipped. - A path with the wrong number of `../` segments that happened to work under `DEBUG = True`. Fixes, in order of preference: ship the missing file, correct the path, strip the stale source-map comment in the build, or (last) subclass the storage and narrow `patterns` for that vendor directory. | Failure | Exception at the source | What collectstatic shows | |---|---|---| | Referenced file missing | `ValueError` | `CommandError` with the file and line note | | Circular references | `RuntimeError` | `CommandError`: Max post-process passes exceeded | | Non-UTF-8 CSS or JS file | `UnicodeDecodeError` | `CommandError` for that file |
- Why does Django hash a stylesheet after rewriting it rather than before?The stylesheet's content includes the names of the files it references. Hashing the rewritten bytes means a changed image or font changes the stylesheet's name too, so browsers holding the old CSS fetch the new one and, through it, the new asset. Hashing the source would leave the CSS name unchanged and strand the new image.
- A third-party CSS file breaks collectstatic because it references a font you do not use; what are your options?Prefer shipping the font or deleting the rule in your copy of the vendor file, since either keeps full rewriting. If you cannot touch the file, a subclass can override `patterns` or `url_converter` to skip that reference. Switching the whole project back to `StaticFilesStorage` gives up cache busting everywhere to hide one broken link.
saying these in an interview costs you the question
- Only the CSS file's own source bytes are hashed, so image changes never rename it.
- Absolute https: and data: URLs inside CSS are rewritten to hashed names too.
- JavaScript import statements are always rewritten by the default storage.
- A missing referenced file is skipped with a warning and collectstatic continues.
- Raising max_post_process_passes fixes circular @import chains.