Skip to content

Bundle and minify static CSS/JS with django-compressor - #676

Draft
bpepple wants to merge 7 commits into
masterfrom
django-compressor
Draft

bpepple wants to merge 7 commits into
masterfrom
django-compressor

Conversation

@bpepple

@bpepple bpepple commented Nov 5, 2026

Copy link
Copy Markdown
Member

Summary

Adds django-compressor so production serves local CSS and JS as minified bundles with content-hashed filenames. Pages make fewer requests, and every deploy changes the filenames, so browsers fetch new assets instead of using cached copies from Spaces (cached for 7 days).

Development and tests are unchanged: compression is off whenever DEBUG is on, so templates load the original files.

Changes

  • Dependency: django-compressor 4.6.0
  • Settings:
    • Register the compressor app and CompressorFinder
    • Production compresses offline and writes the bundles and CACHE/manifest.json to the Spaces bucket
    • Set COMPRESS_ROOT explicitly because production has no STATIC_ROOT
    • Set the offline manifest storage name explicitly. 4.6.0 defaults to the misspelled "compressor-offine"; this is fixed upstream in #1325 but not released yet.
  • Templates:
    • base.html bundles base.css, izaya/layout.css and izaya.css into one stylesheet; izaya.js keeps defer
    • Bundle page-level assets on home, timeline, statistics, the issue and attribution forms, and the collection form, detail and stats pages
    • {% bulma %}, {% font_awesome %}, CDN scripts and inline scripts that use template variables stay outside the bundles
    • issue-form-usability.js now loads next to bulma-calendar so the two share a bundle (it doesn't depend on the inline form script)
  • Tests: tests/metron/test_compress.py checks that the home page uses bundles when compression is on and the original files when it's off
  • Docs: DEPLOYMENT.md adds the compress step

Deployment

manage.py compress --force must run on every deploy. In production, any page whose {% compress %} block isn't in the manifest fails with OfflineGenerationError, and that includes the first deploy before any manifest exists.

Run it from the new image before restarting:

podman run --rm --network host --env-file ~/.config/containers/metron.env \
    localhost/metron:latest python manage.py compress --force
systemctl --user restart metron-web metron-anubis

Each gunicorn worker caches the manifest in memory, so restart after compressing. The "Invalid template django_filters/rest_framework/crispy_form.html" warning that compress prints is harmless.

Testing

  • pytest: 2240 passed, 2 skipped
  • ruff check and ruff format --check pass
  • A local offline compress --force built 10 bundles from 156 templates with no errors
  • Production settings (dummy credentials) resolve compressor's bundle and manifest storage to S3
  • Browser check with compression on: styles, Font Awesome icons, the date picker, izaya lookups, and charts
  • After deploying, confirm static/CACHE/manifest.json exists in the Spaces bucket

Register the compressor app and its static-file finder. Compression stays off whenever DEBUG is on (development and tests). In production it runs offline and writes bundles and the manifest (CACHE/manifest.json) to the Spaces bucket.

Set COMPRESS_ROOT explicitly because production has no STATIC_ROOT. Also pin the offline manifest storage name, since django-compressor 4.6 defaults to the misspelled "compressor-offine".
Wrap each template's local stylesheet and script tags in {% compress %} blocks so production serves minified bundles with content-hashed filenames, which also busts the browser cache on each deploy.

- base.html: bundle base.css, izaya/layout.css and izaya.css into one stylesheet, and compress izaya.js (it keeps its defer attribute)
- Bundle page-level assets on home, timeline, statistics, the issue and attribution forms, and the collection form, detail and stats pages
- Load issue-form-usability.js next to bulma-calendar so the two share a bundle; it doesn't depend on the inline form script
- collection_stats: bundle Chart.js, the inline dark-mode script and chartkick in their original order

{% bulma %}, {% font_awesome %}, CDN scripts and inline scripts that use template variables stay outside the compress blocks.
Production compresses offline. If compress hasn't been run for the current templates, pages fail with OfflineGenerationError, so run it on every deploy.

- Fresh install: run compress after collectstatic, then restart metron-web, because each gunicorn worker caches the manifest in memory
- Updates: run compress from the newly built image in a one-off container before restarting, so workers never start against a stale manifest; collectstatic moves to the same one-off form
- Mention the harmless django-filters crispy_form.html warning
@bpepple bpepple self-assigned this Nov 5, 2026
@bpepple bpepple added the enhancement New feature or request label Nov 5, 2026
- Set COMPRESS_URL to an absolute https:// URL. S3Boto3Storage emits https:// asset URLs, so the scheme-less STATIC_URL made offline compress reject every file.
- Give each release its own offline manifest (CACHE/manifest-<METRON_RELEASE>.json). The value comes from a new Containerfile build arg, so running compress during a deploy no longer changes the manifest that running containers read, and rollbacks find their manifest still in place. Update DEPLOYMENT.md to pass the arg.
- Wrap the bulma-calendar tags in issue_filter.html in compress blocks.
- Put Chart.js, chartkick and bulma-calendar in their own compress blocks so each library keeps one URL across pages and the browser can reuse its cached copy.
- Tests: assert on the actual Bulma stylesheet link, and give compressor an isolated in-memory cache instead of the shared Redis cache.
@bpepple
bpepple marked this pull request as draft November 5, 2026 16:24
Add an "Upgrading PostgreSQL" section to DEPLOYMENT.md covering routine minor updates and a 16 -> 18 dump/restore procedure.

The major upgrade steps cover two things that a plain restore gets wrong:

- The postgres:18 image moved PGDATA to /var/lib/postgresql/18/docker, so the volume must be mounted at /var/lib/postgresql.
- pg_dump doesn't save the ALTER FUNCTION unaccent(text) IMMUTABLE change from migration 0050, because unaccent(text) belongs to an extension. Without redoing it before the restore, the *_name_unaccent_trgm_idx indexes fail to build and pg_restore keeps going anyway.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant