#3179·emdash

Seeded gallery blocks lose their media on first edit: $media asset shape is stripped by sanitizeGalleryImages

Author: SailingGregCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbot:bugbot:needs-attention

Description

A gallery block seeded with $media is silently destroyed the first time the entry is opened in the content editor. The images are not merely unpreviewable — the asset references are stripped and autosaved back, so the gallery permanently loses its media unless the stored content is repaired by hand.

Cause

$media resolves a gallery image's asset to a media-provider descriptor:

json
{
  "_type": "image",
  "_key": "gp0i0",
  "asset": {
    "provider": "local",
    "id": "01M2QZRZTZPBJ2WV8A3B61ZZX7",
    "alt": "…",
    "width": 1400,
    "height": 963,
    "mimeType": "image/jpeg",
    "filename": "historic-1.jpg",
    "meta": { "storageKey": "01M2QZRZR8HDNT3039QNQ95B9D.jpg" }
  },
  "alt": "…"
}

sanitizeGalleryImages in packages/core/src/content/converters/gallery.ts keeps only three keys off an asset, and _ref is the only identifier among them:

typescript
asset: {
  _type: "reference",
  _ref: typeof assetRecord._ref === "string" ? assetRecord._ref : "",
  ...(typeof assetRecord.url === "string" && assetRecord.url ? { url: assetRecord.url } : {}),
  ...(typeof assetRecord.provider === "string" && assetRecord.provider
    ? { provider: assetRecord.provider }
    : {}),
},

id and meta.storageKey are dropped, and neither _ref nor url was present, so the round-trip yields:

json
"asset": { "_type": "reference", "_ref": "", "provider": "local" }

galleryImageUrl() in packages/admin/src/components/editor/GalleryNode.tsx then returns "" (no url, no _ref), so the NodeView renders <img src="">.

Why this is data loss, not just a missing preview

The stripped shape is written back by autosave. In my case the draft revision had already been overwritten before I noticed, and every subsequent autosave wrote another revision carrying "_ref": "" — 31 broken image references across the draft and revision history for three pages. Recovery required matching each broken image back to the published row by _key and rewriting the asset.

It is easy to miss, because the public site keeps rendering correctly: ec_pages.content still holds the good shape until the entry is published from the poisoned draft, and a site whose gallery component falls back to asset.meta.storageKey shows nothing wrong at all. The failure is confined to the editor until the moment it is published.

Note on the seed's own contract

emdash seed --validate passes this content, and the seed applies without warning — $media is documented for image fields, and it does produce a shape ImageFieldRenderer is happy with (it reads meta.storageKey). The mismatch is only with the gallery block's stricter asset contract. Either $media should emit _ref/url when it resolves a gallery image, or the seed should reject $media inside a gallery block rather than accept content the editor will destroy.

Steps to reproduce

  1. Create a seed with a gallery block whose images use $media:
json
{
  "_type": "gallery",
  "_key": "gal1",
  "columns": 3,
  "images": [
    {
      "_type": "image",
      "_key": "img1",
      "asset": { "$media": { "url": "https://example.com/photo.jpg", "alt": "A photo", "filename": "photo.jpg" } },
      "alt": "A photo"
    }
  ]
}
  1. npx emdash seed seed.json --validate — passes.
  2. npx emdash seed seed.json — applies cleanly; the media row is created correctly (image/jpeg, real dimensions, status: ready) and the public page renders the image.
  3. Open the entry in the admin content editor.

Expected: the gallery previews the images.

Actual: the gallery block, columns and per-image alt text all load correctly, but each image renders as a broken image with its alt text — <img> with an empty src. Inspecting the entry's draft revision afterwards shows every asset reduced to {"_type": "reference", "_ref": "", "provider": "local"}; the media reference is gone and cannot be recovered from the draft.

Environment

  • emdash 0.38.0
  • astro 6.1.3, @astrojs/node 10.0.4 (mode: "standalone"), @astrojs/react 5.0.2
  • Node.js 22.22.2, pnpm 10.33.0
  • Database: SQLite via better-sqlite3 12.8.0, local media storage
  • OS: Debian (arm64)

Screenshots

Not attached — the affected instance is a client demo. The rendering is precisely: the Gallery detail panel opens normally with the correct Columns value and the correct number of image slots, each slot showing the broken-image glyph followed by that image's alt text, both in the editor body and in the panel's thumbnail strip. The underlying markup is <img src="" alt="…">.

Logs / error output

No errors are logged server-side or in the browser console — an empty src fails silently. The evidence is in the stored content.

Before opening the editor (ec_pages.content, seeded):

json
"asset": { "provider": "local", "id": "01M2QZRZTZ…", "meta": { "storageKey": "01M2QZRZR8….jpg" },}

After opening the editor (draft revision, autosaved):

json
"asset": { "_type": "reference", "_ref": "", "provider": "local" }

For comparison, galleries produced by the WordPress importer carry {"_type": "reference", "_ref": "…", "url": "/_emdash/api/media/file/….jpg"} and survive the round-trip unharmed — which is why this only affects seeded galleries.