---
title: Static Asset Hash Manifest System
---

# Static Asset Hash Manifest System

## Quick Start Guide

### TL;DR for Developers

**No changes needed for daily development!** Just add assets to `/static` and the system handles everything.

```bash
# 1. Add your asset (fonts, images, videos, etc.)
cp my-icon.svg ui/app-ui/public/static/

# 2. Use it in code
# CSS: url('/static/my-icon.svg')  (auto-transformed)
# JSX: staticAssetUrl('my-icon.svg')

# 3. Build
pnpm build  # Automatically generates hashes
```

---

## Overview

**Problem**: Assets are cached for 1 year (`immutable`), so users get stuck with old versions when files update.

**Solution**: Content-based hashing automatically changes URLs when content changes, forcing cache invalidation.

```
Asset updates → New hash → New URL → Cache miss → User gets latest version
```

### Directory Structure

```
ui/app-ui/public/
├── static/              👈 Source assets (dev & prod)
│   ├── my-font.woff
│   ├── logo.svg
│   └── hero-bg.jpg
│
├── static-p/            👈 Production hashed (auto-generated)
│   ├── my-font.abc12345.woff     (immutable, 1-year cache)
│   ├── logo.def67890.svg
│   └── hero-bg.c8a9e1f4.jpg
```

### How It Works

```mermaid
graph TB
    A[Developer adds asset to /static] --> B{Environment?}

    B -->|Development| C[Serve from /static/asset.ext]
    C --> D[No hashing - fast iteration]

    B -->|Production Build| E[Generate manifest]
    E --> F[Hash content: SHA-256 → abc12345]
    F --> G[Copy to /static-p/asset.abc12345.ext]
    G --> H[Transform CSS & JSX URLs]
    H --> I[Deploy with immutable cache]

    style C fill:#2d5016
    style D fill:#2d5016
    style I fill:#1e4a7a
```

---

## Usage Patterns

### Pattern 1: CSS @font-face (Auto-Transformed)

```css
/* ✅ Source CSS (you write) */
@font-face {
  font-family: 'My Font';
  src: url('/static/my-font.woff');
}

/* 🔄 After Build (auto-transformed in prod) */
@font-face {
  font-family: 'My Font';
  src: url('/static-p/my-font.a3f892c1.woff');
}
```

### Pattern 2: JSX with Runtime Helper

```tsx
import { staticAssetUrl } from '@/utils/staticAssetUrl';

// ✅ Font preload
<link
  rel="preload"
  href={staticAssetUrl('my-font.woff')}
  as="font"
/>

// ✅ Image in component
<img src={staticAssetUrl('logo.svg')} alt="Logo" />

// 🔄 Resolves to (dev)
<link rel="preload" href="/static/my-font.woff" />
<img src="/static/logo.svg" alt="Logo" />

// 🔄 Resolves to (prod)
<link rel="preload" href="/static-p/my-font.a3f892c1.woff" />
<img src="/static-p/logo.def67890.svg" alt="Logo" />
```

### Pattern 3: Background Images in CSS

```css
/* ✅ Source CSS (you write) */
.hero {
  background-image: url('/static/hero-bg.jpg');
}

/* 🔄 After Build (auto-transformed in prod) */
.hero {
  background-image: url('/static-p/hero-bg.c8a9e1f4.jpg');
}
```

---

## Cache Invalidation & Cleanup

```mermaid
graph LR
    A[Update asset] --> B[Build]
    B --> C{Content changed?}

    C -->|Yes| D[New hash: def67890]
    C -->|No| E[Same hash: abc12345]

    D --> F[New URL in HTML/CSS]
    E --> G[Same URL in HTML/CSS]

    F --> H[Browser cache miss]
    G --> I[Browser uses cache]

    H --> J[✅ Users get new version]
    I --> K[✅ Efficient: cached version]

    style D fill:#8b2252
    style F fill:#8b2252
    style H fill:#2d5016
    style J fill:#2d5016
```

**Old hash cleanup:**

- Old hashed files kept for 24 hours after replacement
- Ensures CDN caches have time to propagate
- Automatically deleted after retention period

---

## Manifest Format

### asset-manifest.json (Simple Mapping)

```json
{
  "PPNeueMontreal-Regular.woff": "PPNeueMontreal-Regular.a3f892c1.woff",
  "PPNeueMontreal-Medium.woff": "PPNeueMontreal-Medium.b7e4d3f2.woff",
  "hero-bg.jpg": "hero-bg.c8a9e1f4.jpg"
}
```

### asset-manifest-history.json (Tracking Old Versions)

```json
{
  "PPNeueMontreal-Regular.woff": {
    "current": "a3f892c1",
    "hashes": [
      {
        "hash": "a3f892c1",
        "firstSeen": 1762539826120,
        "replacedAt": null // 👈 Current version
      },
      {
        "hash": "d7c2e5b3",
        "firstSeen": 1762453426120,
        "replacedAt": 1762539826120 // 👈 Replaced 1 day ago (will be deleted)
      }
    ]
  }
}
```

---

## Available Commands

```bash
# 📦 Generate manifest (part of pnpm build)
pnpm manifest:generate

# 🔍 Preview without creating files
pnpm manifest:check

# 🐛 Debug and verify
pnpm manifest:debug

# 🧹 Force clean all old hashes
pnpm manifest:clean
```

### Debug Output Example

```bash
$ pnpm manifest:debug

📦 Asset Details
────────────────────────────────────────────────────────────

PPNeueMontreal-Regular.woff
→ PPNeueMontreal-Regular.a3f892c1.woff
  Original file (/static):   ✅ exists
  Hashed file (/static-p):   ✅ exists
  Hash:          ✅ matches content (a3f892c1)
  Size:          58.27 KB

📜 Hash History
────────────────────────────────────────────────────────────
  Current hash: a3f892c1
  History (2 versions):
    a3f892c1 (current) - first seen 11/7/2025
    d7c2e5b3 (replaced 0.8 days ago on 11/6/2025)

✅ All assets valid and ready for deployment!
```

---

## Debugging Guide

### Asset Not Loading in Production?

Run through this checklist:

```bash
# 1. Check manifest includes your asset
cat src/asset-manifest.json | grep "your-asset"

# 2. Verify hashed file exists
ls public/static-p/your-asset.*

# 3. Check CSS was transformed (if using CSS)
cat dist/tailwind-reload-workaround.css | grep "static-p"

# 4. Use manifest debug tool
pnpm manifest:debug
```

**Common issues:**

- **Asset missing from manifest**: Not in `/static/` directory - add it and rebuild
- **Build incomplete**: Re-run `pnpm build`
- **404 in Network tab**: URL mismatch in code - verify you're using correct path
- **Still seeing old version**: Hard refresh or check if you're testing in dev mode

---

## Configuration Files

### Cache Headers (next.config.mjs)

```javascript
async headers() {
  return [
    {
      source: '/static/(.*)',
      headers: [{
        key: 'Cache-Control',
        value: 'public, max-age=3600',  // 1 hour (dev-like)
      }],
    },
    {
      source: '/static-p/(.*)',
      headers: [{
        key: 'Cache-Control',
        value: 'public, max-age=31536000, immutable',  // 1 year
      }],
    },
  ];
}
```

---

## FAQ

<details>
<summary><strong>Q: Do I need to generate the manifest in dev?</strong></summary>

**A:** No! Dev skips manifest generation completely. Assets are served directly from `/static` with no hashing overhead.

</details>

<details>
<summary><strong>Q: What happens if I forget to add an asset to `/static`?</strong></summary>

**A:** The system gracefully falls back to unhashed URLs with a console warning. The app won't break, but you'll miss out on cache benefits.

</details>

<details>
<summary><strong>Q: Can I use this for images and videos?</strong></summary>

**A:** Yes! The system supports:

- Fonts: `.woff`, `.woff2`, `.otf`, `.ttf`, `.eot`
- Images: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.ico`
- Videos: `.mp4`, `.webm`
- Audio: `.mp3`, `.opus`, `.wav`

</details>

<details>
<summary><strong>Q: How do I test cache invalidation works?</strong></summary>

**A:**

1. Note current hash: `cat src/asset-manifest.json`
2. Update your asset file
3. Rebuild: `pnpm build`
4. Check new hash (should be different)
5. Deploy to preview
6. DevTools Network tab should show new URL

</details>

<details>
<summary><strong>Q: Why keep old hashes for 1 day?</strong></summary>

**A:** Gradual rollout. CDN caches need time to propagate. If a user visits during deployment with an old HTML cache, the old asset URL still works.

</details>

---

## Related Files

### Core Implementation

- [`scripts/generate-asset-manifest.mjs`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/scripts/generate-asset-manifest.mjs) - Hash generator
- [`scripts/transform-css-assets.mjs`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/scripts/transform-css-assets.mjs) - CSS replacement script for hashed URLs
- [`src/utils/staticAssetUrl.ts`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/src/utils/staticAssetUrl.ts) - Runtime helper
- [`scripts/debug-manifest.mjs`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/scripts/debug-manifest.mjs) - Debug utility

### Configuration

- [`next.config.mjs`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/next.config.mjs) - Cache headers
- [`package.json`](https://github.com/suno-ai/glockenspiel/blob/main/ui/app-ui/package.json) - Build scripts
