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
-
Fetches the asset catalogue from the Reepolee backend at
{REEPOLEE_API_URL}/system/imagesand{REEPOLEE_API_URL}/system/files. The API returns paginated JSON with each asset'ss3_key(relative path) andupdated_attimestamp. -
Compares against local copies stored under
assets/images/dynamic/andassets/files/dynamic/. Metadata (theupdated_atfield from Reepolee) is preserved as the file's mtime, so the sync can detect changes by comparing mtimes. -
Downloads changed assets to
assets/{kind}/dynamic/{s3_key}. Theupdated_attimestamp is set as the file's mtime so subsequent runs can skip it. -
Copies files to the public tree. Image files are served through the responsive-image pipeline (
prepare_images.ts), so they're stored underassets/images/dynamic/as originals. File assets (PDFs, documents) are also copied tosrc/public/files/dynamic/so they're directly served without further processing. -
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.tsso stale width-stepped files don't linger.
CLI Options
bun scripts/sync_dynamic_assets.ts [--force]
| Flag | Description |
|---|---|
--force | Re-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
| Counter | Meaning |
|---|---|
added | New asset downloaded that didn't exist locally |
updated | Existing asset re-downloaded because mtime didn't match |
deleted | Local asset removed because it's no longer in the catalogue |
unchanged | Local 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
| Variable | Required | Description |
|---|---|---|
REEPOLEE_API_URL | Yes | Origin 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.