Dynamic Asset Sync

scripts/sync_dynamic_assets.ts pulls images and files managed by a Reepolee backend into a ReeWeb project so they ship as static files in the SSG build. It reads the Reepolee origin from the REEPOLEE_API_URL environment variable and skips silently when that variable is unset — a standalone ReeWeb site is unaffected.

The script runs first in both bun dev and bun ssg, and is incremental: subsequent runs only download new or changed assets, keeping dev startup near-instant.

bun dynamic:sync           # incremental (skips unchanged assets)
bun dynamic:sync --force   # re-download everything

How It Works

  1. Fetches the asset catalogue from the Reepolee backend at {REEPOLEE_API_URL}/system/images and {REEPOLEE_API_URL}/system/files. The API returns paginated JSON with each asset's s3_key (relative path) and updated_at timestamp.

  2. Compares against local copies stored under assets/images/dynamic/ and assets/files/dynamic/. Metadata (the updated_at field from Reepolee) is preserved as the file's mtime, so the sync can detect changes by comparing mtimes.

  3. Downloads changed assets to assets/{kind}/dynamic/{s3_key}. The updated_at timestamp is set as the file's mtime so subsequent runs can skip it.

  4. Copies files to the public tree. Image files are served through the responsive-image pipeline (prepare_images.ts), so they're stored under assets/images/dynamic/ as originals. File assets (PDFs, documents) are also copied to src/public/files/dynamic/ so they're directly served without further processing.

  5. Deletes stale local assets that are no longer in the Reepolee catalogue. For images, it also cleans up any responsive variants generated by prepare_images.ts so stale width-stepped files don't linger.

CLI Options

bun scripts/sync_dynamic_assets.ts [--force]
FlagDescription
--forceRe-download every asset even when the local copy appears current

No --public or --dist flags — the script resolves paths relative to the project root automatically.

Output

Dynamic assets synchronized:
  Images: 3 added, 0 updated, 1 deleted, 12 unchanged
  Files:  1 added, 0 updated, 0 deleted, 4 unchanged
CounterMeaning
addedNew asset downloaded that didn't exist locally
updatedExisting asset re-downloaded because mtime didn't match
deletedLocal asset removed because it's no longer in the catalogue
unchangedLocal asset skipped — mtime matches the catalogue

Directory Layout

project/
├── assets/
│   ├── images/
│   │   ├── hero.png              ← committed original (your own images)
│   │   └── dynamic/
│   │       ├── product-a.jpg     ← synced from Reepolee
│   │       └── banner.webp       ← synced from Reepolee
│   └── files/
│       └── dynamic/
│           └── brochure.pdf      ← synced from Reepolee
│
├── src/public/
│   ├── images/
│   │   └── responsive/           ← generated by prepare_images.ts (includes dynamic images)
│   └── files/
│       └── dynamic/
│           └── brochure.pdf      ← copy for direct serving (dynamic files only)

Dynamic images land in assets/images/dynamic/ so prepare_images.ts picks them up alongside your committed originals — they get the same width-stepped WebP + JPEG treatment. Dynamic files skip the image pipeline and go straight to src/public/files/dynamic/ for direct serving.

The assets/ directory (including dynamic/) is meant to be committed — the sync writes here so the SSG build has the files without a runtime dependency on the Reepolee backend. .gitignore the src/public/images/responsive/ directory (generated), not assets/.

Incremental Behaviour

The sync uses mtime-based comparison with a 1-second tolerance to avoid re-downloading assets that differ only due to filesystem timestamp rounding:

  • New asset (no local file) → downloaded
  • Changed asset (mtime difference > 1s) → downloaded
  • Unchanged asset (mtime within 1s) → skipped
  • Deleted asset (no longer in catalogue) → local file removed, responsive variants cleaned up

This makes bun dev fast: the sync runs on startup, skips everything that's current, and the dev server is ready in under a second.

Pass --force to bypass the mtime check and re-download everything — useful after a database restore or when you suspect filesystem clock drift.

Responsive Image Cleanup

When a dynamic image is deleted from the Reepolee catalogue, the sync removes not only the original under assets/images/dynamic/ but also any matching width-stepped variants under src/public/images/responsive/. This prevents stale 300/, 500/, 800/, 1440/ variants from lingering after the source is gone.

The matching logic strips width-prefix segments from responsive output paths and compares the remaining stem against the dynamic asset key — so responsive/300/product-a.webp and responsive/product-a.webp both match dynamic/product-a.jpg and are cleaned up together.

This cleanup runs only for dynamic images, not your own committed originals — the sync never touches responsive variants of non-dynamic assets.

Environment

VariableRequiredDescription
REEPOLEE_API_URLYesOrigin of the Reepolee backend, e.g. https://app.example.com

When REEPOLEE_API_URL is unset or empty, the script prints a skip message and exits with code 0 — no error, no warning, just a clean skip. This is designed so the same project works as a standalone ReeWeb site and as a paired frontend without configuration changes.

SSG Wiring

bun dynamic:sync is the first step in both bun ssg and bun dev:

bun ssg:    dynamic:sync → prepare:images → css:build → ssg.ts → og:images → sitemap → rss → ssg:search
bun dev:    dynamic:sync → prepare:images → css:build → [orchestrator starts dev server + Tailwind watcher]

In bun ssg it runs blocking — assets must be present before the build renders pages that reference them. In bun dev it runs as part of the one-shot setup before the orchestrator spawns the dev server, so missing assets are fetched before the first page view.

Relationship to ReeWeb & Reepolee

This script is the static bridge between a Reepolee backend and a ReeWeb frontend. Reepolee manages images and files through its admin panel (uploads, crops, resizes) and stores them in its own storage (S3 or local disk). The sync pulls those assets into the ReeWeb project so the static site generator can include them without a runtime HTTP dependency on the backend.

See ReeWeb & Reepolee for the broader data-source model — this script handles the asset half; the fetch() route-loaders handle the data half.