Skip to content

Fix CSS fingerprinting to happen after PurgeCSS #16274

@CamSoper

Description

@CamSoper

Problem

The current build process has an architectural flaw that causes CloudFront cache issues when HTML templates change but CSS source files don't.

Current Build Process

  1. Webpack generates assets/css/bundle.css (4.2MB with all Tailwind utilities)
  2. Hugo runs {{ $css := resources.Get "css/bundle.css" | fingerprint }}:
    • Computes SHA-256 hash from the 4.2MB source file
    • Copies to public/css/bundle.<hash>.css
    • Embeds hash in HTML: <link href="/css/bundle.<hash>.css">
  3. PurgeCSS runs scripts/minify-css.js:
    • Scans public/**/*.html to find used CSS classes
    • Removes unused classes
    • Modifies the file in-place in public/css/
    • File content changes dramatically (4.2MB → 350KB), but filename stays the same

The Issue

The filename hash is based on the source CSS (before PurgeCSS), but CloudFront serves the purged CSS (after PurgeCSS). This creates a mismatch:

  • Scenario: A new HTML template is added (e.g., reinvent page with -translate-y-1/2 class)
  • Source CSS: Unchanged (Tailwind already has the utility with purge: false)
  • Hugo fingerprint: Same hash 34a92c... (computed from unchanged source)
  • PurgeCSS output: Different content (now keeps -translate-y-1/2 because it sees it in HTML)
  • Result: Same filename bundle.34a92c....css, but different content
  • CloudFront: Caches old version for 1 year, serves stale CSS

Real-World Impact

This happened with PR #16230 (reinvent 2025 page):

  • Production CSS: 357,582 bytes, missing -translate-y-1/2 class
  • Local CSS: 358,401 bytes, contains the class
  • Same filename: bundle.34a92c610741a649ee861f5906ac268dfe493817c24e40a169872e8ff4437da4.css
  • CloudFront served 20+ hour old cached version

Workarounds Used

Immediate fix: Modified theme/tailwind.config.js to force a new fingerprint (this PR/commit)

Why it works: Changing Tailwind config → new source CSS → new fingerprint → new filename → CloudFront cache miss

Proper Long-Term Solutions

Option 1: Post-Build Fingerprinting (Recommended)

Compute fingerprint after PurgeCSS runs:

# build-site.sh
hugo build  # Don't fingerprint CSS yet
yarn run minify-css  # PurgeCSS runs
node scripts/fingerprint-and-update-html.js  # Compute hash, rename CSS, update HTML

Pros:

  • Fingerprint perfectly matches served content
  • No cache issues
  • Clean architecture

Cons:

  • Requires post-processing 4,918 HTML files to update CSS references
  • Estimated work: 1-2 days

Option 2: CloudFront Cache Invalidation

Add invalidation to deployment:

# scripts/ci-push.sh
./scripts/run-pulumi.sh update
./scripts/invalidate-cdn-cache.sh  # New script

Pros:

  • Simple to implement (1-2 hours)
  • No architectural changes

Cons:

  • Doesn't fix root cause
  • Invalidations take 1-5 minutes to propagate
  • First 1000 invalidations/month are free, then $0.005 each

Option 3: Disable PurgeCSS

Ship the full 4.2MB CSS file:

Pros:

  • Perfect fingerprinting
  • Zero cache issues

Cons:

  • 12x larger CSS download
  • Not acceptable for production

Recommendation

Implement Option 2 (CloudFront invalidation) as a short-term fix, then Option 1 (post-build fingerprinting) as the proper long-term solution when time permits.

Related Files

  • layouts/partials/assets.html - Where Hugo fingerprinting happens
  • scripts/build-site.sh - Build orchestration
  • scripts/minify-css.js - PurgeCSS implementation
  • infrastructure/index.ts - CloudFront configuration (lines 503-506, 509-512)

References

  • Original issue: Reinvent page CSS not working in production
  • Root cause investigation: CSS fingerprint doesn't match purged content
  • Immediate fix: Force new fingerprint via Tailwind config change

Metadata

Metadata

Assignees

Labels

area/docsImprovements or additions to documentationdocs-opskind/bugSome behavior is incorrect or out of spec

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions