Styling — Dark Theme

Website Rebuild with Express

Chapter 5 · Styling — Dark Theme

Every sibling course in this series had some asset story, however minimal. Express's own is the most minimal of all six.

Serving a Plain Stylesheet

// server.js app.use(express.static('public'));
/* public/css/global.css */ body { background: #0f1117; color: #c9d1d9; font-family: system-ui, sans-serif; margin: 0; }
<!-- views/partials/header.ejs --> <link rel="stylesheet" href="/css/global.css">

Genuinely More Minimal Than Even Rails' Propshaft

Zero processing, not just minimal processing
Rails Rebuild's own Chapter 5 called Propshaft minimal — but Propshaft still does exactly one real thing: content-hash fingerprinting for cache-busting. express.static() does none of that. It's a literal raw static-file server, serving bytes from disk unchanged, with no fingerprinting, no cache-busting, no compilation step of any kind — Express's own standard middleware for a job every basic web server already knows how to do, offered here with zero enhancement on top.

A Real, Honest Operational Gap

No cache-busting exists at all
Since express.static() serves the file at the exact same URL every time, a visitor's browser (or an intermediate proxy) may keep serving a stale, cached copy of global.css after an update, with nothing in this setup to signal that the file changed. A manual versioned query string — /css/global.css?v=2, bumped by hand on every real change — is the honest, low-tech fix; there's no automatic equivalent to Propshaft's own fingerprinting here.

Scoped Styles Don't Apply Here at All

Astro's own scoped <style> blocks worked because Astro has real components with real boundaries to scope styles to. EJS partials have no such boundary — include() just inlines one template's own output into another's, so the entire concept of "scoping" a style to one partial doesn't have anywhere to attach. This isn't a missing feature so much as a question that doesn't arise in a template-inclusion system the way it does in a component system.

The Full Asset-Bundling Spectrum, Closed Out

Next.jsDjangoLaravelRailsAstroExpress
ApproachBundles by designShips nothingPre-wires VitePropshaft + ImportmapScoped styles + plain importexpress.static()
Cache-busting?Yes, automaticNoYes, via ViteYes — content-hash fingerprintingBuild-time hashingNone — manual only

Hands-On Exercises

Exercise 1

Set up express.static('public'), link global.css from the header partial, and confirm the dark theme applies site-wide.

📄 View solution
Exercise 2

Update global.css, reload without a query-string change, and confirm a browser may still serve the stale cached version — then fix it with a manual ?v= bump.

📄 View solution
Exercise 3

Compare the bytes of a served CSS file directly against the source file on disk, and confirm they're identical — no fingerprinting, hashing, or transformation applied at all.

📄 View solution

Chapter 5 Quick Reference

  • express.static('public') — a raw static-file server, zero processing
  • More minimal than Rails' Propshaft — no fingerprinting, no cache-busting at all
  • Real gap — a manual versioned query string is the only cache-busting available
  • Scoped styles don't apply — no component boundary exists to scope them to
  • Next chapter: The Database: Raw SQL, No ORM