Skip to content

Replace Tailwind Play CDN runtime with a proper Tailwind CLI build #99

Description

@sarg3nt

Background

The dashboard currently loads Tailwind via the Play CDN runtime (https://cdn.tailwindcss.com) — the same runtime regardless of which asset mode is enabled:

Asset mode Tailwind source
Production (default, USE_LOCAL_ASSETS=false) <script src="https://cdn.tailwindcss.com">
Local-assets (USE_LOCAL_ASSETS=true) <script src="/static/js/vendor/tailwind.js"> — a curl-downloaded copy of the same Play CDN script (see Makefile dev-assets target line 108)

Both run the full Tailwind engine in the browser and JIT-compile utility classes from a runtime HTML scan. Tailwind itself prints this warning on every page load:

cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in
production, install it as a PostCSS plugin or use the Tailwind CLI:
https://tailwindcss.com/docs/installation

Why this matters

  1. Performance — ~280 KB of Tailwind engine is downloaded and parsed on every cold visit. Every navigation re-runs the JIT compiler against the HTML.
  2. Caching — there's no static CSS file to cache. A user who navigates between five gear pages re-runs the JIT compiler five times.
  3. Production hygiene — the explicit upstream warning is exactly the kind of thing someone deploying the dashboard to a non-homelab environment will notice and flag.
  4. CSP surface — we currently have script-src https://cdn.tailwindcss.com in production CSP. Dropping the runtime drops that allowance.
  5. Class purging — a proper build only emits the utilities actually used. The runtime emits everything that gets JIT-compiled, which is functionally similar but bound to the runtime's correctness.

Proposed approach

Move to the Tailwind CLI (or PostCSS) build, generating a static minified CSS file served from /static/css/.

Concrete steps

  1. Add npm tooling under gearbox/ (this is the only Node dependency we'd need — keep its blast radius small):

    npm init -y
    npm install -D tailwindcss@latest

    gearbox/package.json and gearbox/package-lock.json enter the tree. No runtime npm dependencies — Tailwind CLI is dev-only.

  2. Create gearbox/tailwind.config.js with content paths pointing at every file that uses Tailwind classes:

    module.exports = {
        content: [
            './internal/**/*.templ',
            './internal/**/*.go',  // catch any class names embedded in Go (rare but possible)
            './static/**/*.{js,html}',
        ],
        darkMode: 'class',  // matches the current inline tailwind.config in base.templ
        theme: { extend: { /* lift from current inline config */ } },
    };

    The existing inline tailwind.config = { darkMode: 'class', ... } JS block in base.templ:233-264 ports to this file.

  3. Create the source CSS: gearbox/static/css/tailwind.src.css:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
  4. Add a Makefile target:

    tailwind-build: ## Compile Tailwind CSS
        @npx tailwindcss -i static/css/tailwind.src.css -o static/css/tailwind.css --minify

    Chain it into build and dev so a clean make build always produces a fresh CSS bundle.

  5. Update every template that loads Tailwind to use the bundled file. Eight files known today:

    • internal/framework/templates/layouts/base.templ
    • internal/framework/templates/pages/user_pages.templ (4 occurrences)
    • internal/framework/templates/pages/welcome.templ
    • internal/framework/templates/pages/login.templ
    • internal/framework/templates/pages/haproxy_settings.templ

    Replace:

    <script src="https://cdn.tailwindcss.com"></script>
    <script>tailwind.config = { darkMode: 'class', ... }</script>

    with:

    <link rel="stylesheet" href="/static/css/tailwind.css">

    And delete the inline tailwind.config = {...} blocks (config now lives in tailwind.config.js).

  6. CSP cleanup — drop https://cdn.tailwindcss.com from script-src in internal/framework/middleware/security_headers.go. Strict-CSP mode (already 'self'-only) becomes the consistent default rather than an opt-in.

  7. Delete gearbox/static/js/vendor/tailwind.js (407 KB) and the dev-assets Makefile snippet that downloads it — the local-assets mode now uses the same bundled CSS as production.

  8. CI — add make tailwind-build to the GitHub Actions pipeline so tailwind.css is regenerated on every build rather than committed. Or commit the generated file with a CI check that it matches what a fresh build would produce; pick one and document the choice.

Migration concerns

  • Class scanning — Tailwind's JIT runtime detects classes by parsing the live HTML. The CLI scans static files. Any class name dynamically assembled in JS (e.g. 'kpi-card status-' + status) won't be found unless the prefix or the full set is in a safelist: config or appears verbatim somewhere in the scanned content. Existing JS-side class assembly to audit (rough survey):

    • metrics.templ: 'kpi-card status-' + status — needs safelist: ['status-good', 'status-warn', 'status-bad'] or similar.
    • Sidebar / chip / KPI badges with conditional classes — most use static class strings, but a sweep through static/js/ and the <script> blocks in templ files is needed during the migration.
  • Custom utilities / theme tokens — the inline tailwind.config = {...} in base.templ defines darkMode: 'class' and (per the inline script) probably some extends. All of those need to move to tailwind.config.js. Audit step before the migration starts.

  • Build ordermake build currently goes templ generatego build. Tailwind needs to run between templ generate and go build so it can scan the generated *_templ.go if class names happen to live in Go strings — though scanning *.templ directly is sufficient for the common case.

  • Dev workflowmake dev (hot reload with air) needs to also rebuild Tailwind on *.templ changes. Either air calls make tailwind-build as part of its rebuild command, or run a separate npx tailwindcss --watch alongside.

Out of scope

  • Switching to a different CSS framework.
  • Inlining critical CSS for first paint optimisation.
  • Removing inline <style> blocks in templ files (they currently use Tailwind syntax and the CSS embedded by templ; some of those would simplify if we ran them through Tailwind's CLI too, but that's a separate clean-up).

Definition of done

  • npx tailwindcss builds a static CSS file from source.
  • Every templ that previously loaded the CDN runtime loads the bundled CSS instead.
  • The cdn.tailwindcss.com should not be used in production warning is gone from the browser console.
  • script-src https://cdn.tailwindcss.com removed from production CSP.
  • gearbox/static/js/vendor/tailwind.js (407 KB) deleted along with the dev-assets download step for it.
  • make build produces a working dashboard with no Tailwind runtime loaded.
  • Existing pages render unchanged (visual diff against main).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions