From 698cd60521d5a840bef5fd20ca85879d3b228c3f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 19 Jul 2026 03:02:17 +0000 Subject: [PATCH] Static asset magnification for the Website service (#20) This PR contains the work done for build-time optimization of the static assets served by the Website service. The production Docker image now ships minified JS/CSS and losslessly recompressed images, while the sources in the repository stay readable and un-minified. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/loud-amsterdam/pulls/20 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- Services/Website/Dockerfile | 31 ++++++++++++++++++++++++++++++- Services/Website/Makefile | 11 +++++++++++ Services/Website/README.md | 15 ++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Services/Website/Dockerfile b/Services/Website/Dockerfile index b2ed87f..6f3f1d4 100644 --- a/Services/Website/Dockerfile +++ b/Services/Website/Dockerfile @@ -1,3 +1,27 @@ +# ================================ +# Asset image +# ================================ +FROM node:22-alpine AS assets + +# Install the minifiers in their own layer, so they are cached across asset changes +RUN apk add --no-cache oxipng \ + && npm install --global esbuild svgo + +# Copy the static files and minify the JS/CSS/SVG sources and losslessly recompress +# the PNG images in place, keeping their names so the URL paths derived from the +# StaticFile enumeration stay unchanged. +WORKDIR /static +COPY ./Services/Website/Resources/Static . +RUN esbuild --minify --allow-overwrite --outdir=css css/*.css \ + && esbuild --minify --allow-overwrite --outdir=js js/*.js \ + && oxipng --opt max --strip safe *.png \ + && svgo icon.svg + +# Export stage: `docker build --target assets-export --output ` writes the +# minified static files to for local inspection. +FROM scratch AS assets-export +COPY --from=assets /static / + # ================================ # Build image # ================================ @@ -45,8 +69,13 @@ RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ RUN find -L "$(swift build --package-path /build/Services/Website -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; # Copy the static files directory (served by FileMiddleware) if it exists +RUN [ -d /build/Services/Website/Resources ] && mv /build/Services/Website/Resources ./Resources || true + +# Overwrite the static files with the minified copies from the assets stage +COPY --from=assets /static ./Resources/Static + # Ensure that by default, neither the directory nor any of its contents are writable. -RUN [ -d /build/Services/Website/Resources ] && { mv /build/Services/Website/Resources ./Resources && chmod -R a-w ./Resources; } || true +RUN chmod -R a-w ./Resources # ================================ # Run image diff --git a/Services/Website/Makefile b/Services/Website/Makefile index 47d91c8..2ed486e 100644 --- a/Services/Website/Makefile +++ b/Services/Website/Makefile @@ -105,6 +105,17 @@ db-reset: ## Stop and remove the local database instance and delete its data vol --profile database down mariadb \ --volumes +# --- Assets minification ------------------------------------------------------ + +.PHONY: ast-minify +ast-minify: ## Preview the minified JS/CSS assets in .build/minified + @docker build \ + --target assets-export \ + --output .build/minified \ + --file Dockerfile \ + ../.. + @echo "Minified assets written to .build/minified" + # --- Registry deployment ------------------------------------------------------ .PHONY: img-check diff --git a/Services/Website/README.md b/Services/Website/README.md index 27d7fa6..5f85d3a 100644 --- a/Services/Website/README.md +++ b/Services/Website/README.md @@ -6,7 +6,7 @@ The service: - Serves the landing page at `GET /` (rendered once per supported language with [Elementary](https://github.com/elementary-swift/elementary) and cached). - Negotiates each request's language from its `Accept-Language` header against the languages in the `WebsiteLibrary` String Catalog, falling back to the default (`en`); pages are served from the per-language cache with `Content-Language` and `Vary: Accept-Language` headers. - Answers a liveness check at `GET /health` with a static JSON payload, and a readiness check at `GET /health/ready` that reports whether the database is reachable (`200` ready / `503` unavailable). -- Serves static files (CSS, JS, icons, manifest, `robots.txt`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`. +- Serves static files (CSS, JS, icons, manifest, `robots.txt`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`; the production image ships minified copies (see [Static assets](#static-assets)). - Returns a custom HTML 404 page, localized like the landing page, for any request that matches neither a route nor a static file. - Compresses responses (gzip/deflate) above a configurable size when the client advertises support. - Stamps a hardened set of security headers on every response. @@ -200,6 +200,19 @@ docker compose -f docker-compose.yml pull docker compose -f docker-compose.yml up -d ``` +### Static assets +The image build optimizes the files under `Resources/Static` in its `assets` stage, in place: +- CSS and JS are minified with [esbuild](https://esbuild.github.io). +- PNG images are losslessly recompressed with [oxipng](https://github.com/oxipng/oxipng) — the output is pixel-identical, only encoded smaller. +- The SVG icon is minified with [svgo](https://github.com/svg/svgo). + +Files keep their names and paths, so the URLs derived from the `StaticFile` enumeration are unaffected. The sources in the repository stay readable and unminified: a direct `swift run` serves them as-is, while any image build — including the local `make site-mount` one, which builds the same Dockerfile — serves the optimized copies. + +Preview the optimized output locally — requires only Docker and writes to the git-ignored `.build/minified`: +```sh +make ast-minify +``` + ### Required variables The Makefile and Compose files read these from a `.env` file (or the environment). Provide your own values — do **not** commit secrets. | Variable | Used for |