Skip to content

Frontend & CSS

Zeython is a backend framework — it renders HTML via Jinja2 and Blade (resources/views/, see Views and Blade) and leaves the frontend build tooling up to you. It doesn't bundle a JS framework or a Vite/Node.js pipeline. What it does ship is a real, production-ready Tailwind CSS setup out of the box, because "unstyled HTML" is a bad first impression for a new project, and a <script> tag pointed at a CDN is not something you should actually ship.

What a fresh project has

resources/css/app.css     # Tailwind source -- one line: @import "tailwindcss";
public/css/               # zeython css build writes app.css here (gitignored)

resources/views/welcome.blade.html's <head> picks between two things depending on APP_DEBUG:

@if(debug)
<script src="https://cdn.tailwindcss.com"></script>
@else
<link rel="stylesheet" href="/css/app.css" />
@endif
  • APP_DEBUG=true (the default in a fresh .env): Tailwind's Play CDN. Zero setup, no build step, utility classes just work in any template — but it compiles every class in the browser on every page load, with nothing purged. Fine for local development, never used outside it.
  • APP_DEBUG=false: the compiled stylesheet at /css/app.css, served by a StaticFiles mount already wired up in main.py. Minified, and purged of every class your templates don't actually use.

Building it

zeython css build

Downloads the standalone Tailwind CSS CLI — the real Tailwind compiler, with no Node.js or npm involved — caches it in .tailwind/ (gitignored), and compiles resources/css/app.css to public/css/app.css. Supports Linux (x64/arm64), macOS (Intel/Apple Silicon), and Windows x64; see zeython css build --help for --binary if you're on a platform without a standalone build (e.g. musl-libc Linux like Alpine) and have your own tailwindcss executable to point at instead.

By default this fetches whatever GitHub's latest release tag currently resolves to, over plain HTTPS with no signature or checksum check before running it — fine for a local dev machine, not something a CI pipeline or production build should settle for. Pin it instead:

zeython css build --tailwind-version v3.4.13 --tailwind-sha256 <known-good-digest>
# or via environment variables, e.g. in your CI config:
# TAILWIND_CSS_VERSION=v3.4.13 TAILWIND_CSS_SHA256=<known-good-digest>

--tailwind-version pins the exact release fetched instead of trusting latest not to change under you; --tailwind-sha256 verifies the download against a checksum you computed yourself against a copy you trust, refusing to run anything that doesn't match. Either can be set without the other; both are opt-in so the zero-config default keeps working.

zeython css watch

Same thing, but rebuilds on every change to resources/css/ or any template — leave it running alongside zeython serve while you work, the same way you'd run npm run dev in a Node-based project (just without the npm).

Tailwind v4 is CSS-first: there's no tailwind.config.js to maintain, and utility classes are picked up automatically from every template in the project — no content globs to keep in sync. Customize the design system directly in resources/css/app.css with an @theme block:

@import "tailwindcss";

@theme {
  --color-brand: oklch(0.6 0.2 280);
}

Production images build it for you

The generated Dockerfile runs zeython css build in its builder stage and copies only the compiled public/css/app.css into the final image — not the ~100MB standalone Tailwind binary itself. A docker build (or any CI pipeline that uses this Dockerfile) always ships the compiled, production stylesheet automatically; there's no manual step to remember before deploying.

If you don't want Tailwind at all

Delete resources/css/app.css, the @if(debug)...@endif block in welcome.blade.html's <head>, and the zeython css build line from the Dockerfile. Write plain CSS, or drop in any other framework — nothing else in Zeython depends on it.

A Node-based pipeline instead

If your team already has a Vite/esbuild/webpack setup, there's nothing Zeython-specific about wiring it in: build to a directory (public/css/ or wherever), and it's already mounted at /css in main.py via Starlette's StaticFiles — the same way StorageServiceProvider serves uploads. Point your build's output there, or mount a different path for a different directory.