feat(admin): refine featured image field#2217
Conversation
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | cf5a5d5 | Jul 23 2026, 03:23 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | cf5a5d5 | Jul 23 2026, 03:22 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | cf5a5d5 | Jul 23 2026, 03:23 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
This is a focused, well-scoped admin UI refinement. Opting featured_image into a new variant="featured" keeps the change behind a field-name gate, so ordinary image fields, avatars, and repeaters keep their existing presentation. The Kumo-based card, Lingui-wrapped strings, <bdi dir="ltr"> around dimension metadata, and the added component tests all look good.
The one real gap I found is cross-package: ImageFieldRenderer now persists filename and mimeType (and already persisted blurhash/dominantColor) in ImageFieldValue, but the authoritative core image schemas and generated TypeScript types don't admit those fields. That leaves the public data contract out of sync with the MediaValue type and with what the admin will actually store. I also have a minor hardening suggestion to URL-encode local media path segments the same way FileFieldRenderer does.
What I checked:
- Diff and full changed files (
ImageFieldRenderer.tsx,ContentEditor.tsx, the new test, and the changeset) - Call sites of
ImageFieldRenderer(ContentEditor, RepeaterField, BylineAvatarField) - Core media/value normalization, validation, and type-generation paths
MediaValuevsImageValuevs the runtime Zod schema and staticimage()factory schema
The admin code itself is clean; the blockers are on the core schema/types contract.
Findings
-
[needs fixing]
packages/core/src/schema/zod-generator.ts:135The runtime image schema used by
validateContentDataandvalidateContentonly knows aboutid/src/alt/width/height/provider/previewUrl/meta.ImageFieldRenderer.handleSelectnow writesfilename,mimeType, and already writesblurhash/dominantColor;normalizeMediaValuepreserves them; but this schema (and the generated TS string at line 414) don't, soemdash typesand any consumer validating throughvalidateContentsee an incomplete shape.Update the image case to match
MediaValue:case "image": return z.object({ id: z.string(), src: z.string().optional(), alt: z.string().optional(), width: z.number().optional(), height: z.number().optional(), filename: z.string().optional(), mimeType: z.string().optional(), blurhash: z.string().optional(), dominantColor: z.string().optional(), /** Provider ID (e.g. "local", "cloudflare-images") */ provider: z.string().optional(), /** Admin-side preview URL for external providers (not persisted by plugins) */ previewUrl: z.string().optional(), /** Provider-specific metadata; for local media this carries storageKey */ meta: z.record(z.string(), z.unknown()).optional(), });Then update the
case "image":TypeScript string infieldTypeToTypeScript(line ~414) to include the same optional fields. -
[needs fixing]
packages/core/src/fields/image.ts:5The static
image()factory schema is also out of sync with the value shape the admin now stores: it requiressrcand omitsfilenameandmimeType. Local images are persisted withoutsrc(it is stripped bynormalizeMediaValue), so a code-definedimage()field would reject the same value the admin editor just saved.Bring the factory schema in line with
MediaValue:const imageSchema = z.object({ id: z.string(), src: z.string().optional(), alt: z.string().optional(), width: z.number().optional(), height: z.number().optional(), filename: z.string().optional(), mimeType: z.string().optional(), }); -
[suggestion]
packages/admin/src/components/ImageFieldRenderer.tsx:79The local media URL is built by interpolating
value.meta.storageKeyorvalue.iddirectly into the path, whileFileFieldRendereralreadyencodeURIComponents the same segments for safety. If a storage key ever contains#,?, or other URI-significant characters, the featured-card image request will be misrouted or broken. Encode the path segment to keep the two renderers consistent:? `/_emdash/api/media/file/${encodeURIComponent( typeof value.meta?.storageKey === "string" ? value.meta.storageKey : value.id, )}`
There was a problem hiding this comment.
This is a careful re-review of the featured-image refinement. The cross-package schema issues from the previous review have been resolved:
packages/core/src/schema/zod-generator.tsnow includesfilename,mimeType,blurhash, anddominantColorin both the runtime Zod schema and the generated TypeScript string.packages/core/src/fields/image.tsnow treatssrcas optional and includesfilename,mimeType,blurhash,dominantColor,provider,previewUrl, andmeta, matchingMediaValue.ImageFieldRenderernowencodeURIComponents local media path segments.- Tests were added/updated for both the admin component and the core schemas.
The approach still looks sound: gating the new presentation behind variant="featured" keeps ordinary image fields, avatars, and repeater sub-fields untouched. The Kumo card, Lingui-wrapped strings, <bdi dir="ltr"> around metadata, and the 3:2 aspect ratio all align with the admin UI conventions.
I found two remaining items:
- Missing changeset. The PR modifies published packages (
emdashcore and@emdash-cms/admin), and CONTRIBUTING.md requires a changeset for any published-package behavior change. The.changeset/directory has onlyREADME.mdandconfig.json; no changeset file was added. - Latent null-safety issue in
ImageFieldRenderer. The helper constants usetypeof value === "object", which istruefornull. Current callers coercenulltoundefinedbefore passingvalue, so this doesn't crash today, but the component's ownonChange(null)API makesnulla natural value. The checks should guard againstnullexplicitly so a future caller doesn't hit a runtime error.
Fixing the changeset is the blocking item; the null guard is a small hardening suggestion.
Findings
-
[needs fixing]
.changeset/config.json:1This PR changes published packages (
packages/core/src/fields/image.tsandpackages/core/src/schema/zod-generator.ts, pluspackages/admin/src/components/ImageFieldRenderer.tsx), but no changeset file was added to.changeset/. CONTRIBUTING.md requires a changeset for any published-package behavior change.Run
pnpm changesetand commit the generated Markdown file. The description should describe the observable effect for someone upgrading (e.g., "Adds cachedfilename,mimeType,blurhash, anddominantColorto image field values and updates the admin featured-image card to display the new metadata."), not the implementation details. -
[suggestion]
packages/admin/src/components/ImageFieldRenderer.tsx:114-122typeof value === "object"istruefornull, so theselectedFilename,dimensions, andmimeTypehelpers would throw ifvalueis evernull. Current callers happen to coercenulltoundefinedbefore passing it in, but the component's ownonChange(null)makes that value easy to imagine. Guard againstnullexplicitly:const selectedFilename = value != null && typeof value === "object" && value.filename ? value.filename : t`Selected image`; const dimensions = value != null && typeof value === "object" && typeof value.width === "number" && typeof value.height === "number" ? `${value.width} × ${value.height}` : undefined; const mimeType = value != null && typeof value === "object" && value.mimeType ? value.mimeType : undefined;
|
The required changeset is intentionally committed on this PR’s base branch, This follows the agreed media-library integration workflow: seed the umbrella feature branch and draft PR with the overall feature changeset, then target completed child PRs at that branch. Adding another changeset here would duplicate the release note when the umbrella PR is eventually merged. |
There was a problem hiding this comment.
This is a careful re-review after the previous pass. The cross-package schema issues from before have been resolved: packages/core/src/fields/image.ts, packages/core/src/schema/zod-generator.ts, and ImageFieldRenderer now agree on the MediaValue shape, src is optional for local media, local storage keys are encodeURIComponent-encoded, and tests were added/updated for both the admin component and the core schemas.
The approach still looks sound: gating the new card presentation behind variant="featured" keeps ordinary image fields, avatars, and repeater sub-fields untouched. The Kumo LayerCard, Lingui-wrapped strings, <bdi dir="ltr"> around metadata, and the 3:2 aspect ratio all align with admin UI conventions.
I found two remaining items:
- Missing changeset. The PR modifies published packages (
emdashcore and@emdash-cms/admin), and the changeset directory has onlyREADME.mdandconfig.json. This is the blocking item. - Latent null-safety issue in
ImageFieldRenderer. The helper constants guarding withtypeof value === "object"would throw ifvalueisnull. Callers currently avoid this, but the component'sonChange(null)API makes it easy to imagine a future bug.
No other logic, localization, RTL, or schema-consistency issues jumped out. Fix the changeset and consider the null guard, and this should be good to approve.
Findings
-
[needs fixing]
.changeset/config.json:1This PR changes published packages (
packages/core/src/fields/image.tsandpackages/core/src/schema/zod-generator.ts, pluspackages/admin/src/components/ImageFieldRenderer.tsx), but.changeset/still contains onlyREADME.mdandconfig.json. CONTRIBUTING.md requires a changeset for any published-package behavior change, and bothemdashand@emdash-cms/adminare in the fixed release group here so this change will affect the published CHANGELOG.Run
pnpm changesetand commit the generated Markdown file. Describe the user-visible effect (e.g., "Adds cachedfilename,mimeType,blurhash, anddominantColorto image field values and updates the admin featured-image card to display the new metadata."), not the implementation files. -
[suggestion]
packages/admin/src/components/ImageFieldRenderer.tsx:115The
selectedFilename,dimensions, andmimeTypehelpers guard withtypeof value === "object", which is alsotruefornull. Current callers inContentEditorandRepeaterFieldcoercenulltoundefinedbefore passing it in, so this doesn't crash today, but the component's ownonChange(null)API makesnulla natural value for a future caller. Guard againstnullexplicitly:const selectedFilename = value != null && typeof value === "object" && value.filename ? value.filename : t`Selected image`; const dimensions = value != null && typeof value === "object" && typeof value.width === "number" && typeof value.height === "number" ? `${value.width} × ${value.height}` : undefined; const mimeType = value != null && typeof value === "object" && value.mimeType ? value.mimeType : undefined;
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
What does this PR do?
Refines the post editor's
featured_imagefield into a full-width, responsive Kumo media card with a consistent 3:2 preview, clearer image metadata, and explicit change and remove actions.The featured presentation is opt-in and only applies to
featured_image; ordinary image fields, avatars, and repeater fields retain their existing presentation. Selected media now preserves filename and MIME type alongside the existing dimensions, while legacy and broken-image values continue to degrade gracefully.Closes: N/A
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Verified in the local EmDash demo across desktop and 390px mobile layouts, light and dark themes, and Arabic RTL.
pnpm typecheckpnpm lintpnpm --filter @emdash-cms/admin exec vitest run tests/components/ImageFieldRenderer.test.tsx— 7 tests passedTry this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
feat/editor-featured-image. Updated automatically when the playground redeploys.