diff --git a/.gitignore b/.gitignore index 1e73087..3fee52f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ wheels/ .venv .env + +# Collected static files (generated by `manage.py collectstatic`) +staticfiles/ diff --git a/highscore/app/settings.py b/highscore/app/settings.py index 2354d1b..0ab341f 100644 --- a/highscore/app/settings.py +++ b/highscore/app/settings.py @@ -41,6 +41,7 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -122,3 +123,26 @@ USE_TZ = True # them, so the prefix must be added manually for the admin assets to load under # the /scores sub-path. STATIC_URL = f"{FORCE_SCRIPT_NAME.rstrip('/')}/static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +# WhiteNoise serves static files directly from the app (no separate web server +# needed), with compression and far-future cache headers. In production the +# manifest backend hashes filenames for cache-busting (requires collectstatic); +# locally we skip the manifest and serve straight from the finders so no +# collectstatic step is needed. +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": ( + "whitenoise.storage.CompressedStaticFilesStorage" + if DEBUG + else "whitenoise.storage.CompressedManifestStaticFilesStorage" + ), + }, +} + +# Serve static files from the app dirs in development (we run under daphne, which +# does not auto-serve static like runserver does). +WHITENOISE_USE_FINDERS = DEBUG diff --git a/makefile b/makefile index 999eb09..51b142f 100644 --- a/makefile +++ b/makefile @@ -6,3 +6,4 @@ help: setup: /opt/uv/uv run python highscore/manage.py migrate + /opt/uv/uv run python highscore/manage.py collectstatic diff --git a/pyproject.toml b/pyproject.toml index e28e03a..78ffe40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "django-ninja>=1.6.2", "psycopg>=3.3.4", "pydantic-settings>=2.14.1", + "whitenoise>=6.12.0", ] [project.scripts] diff --git a/uv.lock b/uv.lock index fe5a089..98a310c 100644 --- a/uv.lock +++ b/uv.lock @@ -791,6 +791,7 @@ dependencies = [ { name = "django-ninja" }, { name = "psycopg" }, { name = "pydantic-settings" }, + { name = "whitenoise" }, ] [package.dev-dependencies] @@ -811,6 +812,7 @@ requires-dist = [ { name = "django-ninja", specifier = ">=1.6.2" }, { name = "psycopg", specifier = ">=3.3.4" }, { name = "pydantic-settings", specifier = ">=2.14.1" }, + { name = "whitenoise", specifier = ">=6.12.0" }, ] [package.metadata.requires-dev] @@ -1001,6 +1003,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, ] +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +] + [[package]] name = "zope-interface" version = "8.5"