Skip to content

feat(admin): refine featured image field#2217

Open
khoinguyenpham04 wants to merge 6 commits into
feature/media-libraryfrom
feat/editor-featured-image
Open

feat(admin): refine featured image field#2217
khoinguyenpham04 wants to merge 6 commits into
feature/media-libraryfrom
feat/editor-featured-image

Conversation

@khoinguyenpham04

@khoinguyenpham04 khoinguyenpham04 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Refines the post editor's featured_image field 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

  • Bug fix
  • Feature
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: N/A

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5.6 Sol)

Screenshots / test output

Verified in the local EmDash demo across desktop and 390px mobile layouts, light and dark themes, and Arabic RTL.

  • pnpm typecheck
  • pnpm lint
  • pnpm --filter @emdash-cms/admin exec vitest run tests/components/ImageFieldRenderer.test.tsx — 7 tests passed
  • Admin production build

Try 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.

Copilot AI review requested due to automatic review settings July 23, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cf5a5d5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added the review/awaiting-author Reviewed; waiting on the author to respond label Jul 23, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do cf5a5d5 Jul 23 2026, 03:23 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache cf5a5d5 Jul 23 2026, 03:22 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground cf5a5d5 Jul 23 2026, 03:23 PM

@khoinguyenpham04 khoinguyenpham04 self-assigned this Jul 23, 2026
@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2217

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2217

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2217

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2217

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2217

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2217

emdash

npm i https://pkg.pr.new/emdash@2217

create-emdash

npm i https://pkg.pr.new/create-emdash@2217

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2217

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2217

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2217

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2217

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2217

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2217

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2217

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2217

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2217

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2217

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2217

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2217

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2217

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2217

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2217

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2217

commit: 0b2cc48

@khoinguyenpham04
khoinguyenpham04 changed the base branch from main to feature/media-library July 23, 2026 15:10

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  • MediaValue vs ImageValue vs the runtime Zod schema and static image() 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:135

    The runtime image schema used by validateContentData and validateContent only knows about id/src/alt/width/height/provider/previewUrl/meta. ImageFieldRenderer.handleSelect now writes filename, mimeType, and already writes blurhash/dominantColor; normalizeMediaValue preserves them; but this schema (and the generated TS string at line 414) don't, so emdash types and any consumer validating through validateContent see 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 in fieldTypeToTypeScript (line ~414) to include the same optional fields.

  • [needs fixing] packages/core/src/fields/image.ts:5

    The static image() factory schema is also out of sync with the value shape the admin now stores: it requires src and omits filename and mimeType. Local images are persisted without src (it is stripped by normalizeMediaValue), so a code-defined image() 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:79

    The local media URL is built by interpolating value.meta.storageKey or value.id directly into the path, while FileFieldRenderer already encodeURIComponents 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,
    						)}`
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review cla: signed and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 23, 2026
@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts now includes filename, mimeType, blurhash, and dominantColor in both the runtime Zod schema and the generated TypeScript string.
  • packages/core/src/fields/image.ts now treats src as optional and includes filename, mimeType, blurhash, dominantColor, provider, previewUrl, and meta, matching MediaValue.
  • ImageFieldRenderer now encodeURIComponents 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:

  1. Missing changeset. The PR modifies published packages (emdash core and @emdash-cms/admin), and CONTRIBUTING.md requires a changeset for any published-package behavior change. The .changeset/ directory has only README.md and config.json; no changeset file was added.
  2. Latent null-safety issue in ImageFieldRenderer. The helper constants use typeof value === "object", which is true for null. Current callers coerce null to undefined before passing value, so this doesn't crash today, but the component's own onChange(null) API makes null a natural value. The checks should guard against null explicitly 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:1

    This PR changes published packages (packages/core/src/fields/image.ts and packages/core/src/schema/zod-generator.ts, plus packages/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 changeset and commit the generated Markdown file. The description should describe the observable effect for someone upgrading (e.g., "Adds cached filename, mimeType, blurhash, and dominantColor to 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-122

    typeof value === "object" is true for null, so the selectedFilename, dimensions, and mimeType helpers would throw if value is ever null. Current callers happen to coerce null to undefined before passing it in, but the component's own onChange(null) makes that value easy to imagine. Guard against null explicitly:

    	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;
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 23, 2026
@khoinguyenpham04

khoinguyenpham04 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

The required changeset is intentionally committed on this PR’s base branch, feature/media-library, at .changeset/media-library-project.md. It covers @emdash-cms/admin (minor) and emdash (patch), including this featured-image work.

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.

@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Missing changeset. The PR modifies published packages (emdash core and @emdash-cms/admin), and the changeset directory has only README.md and config.json. This is the blocking item.
  2. Latent null-safety issue in ImageFieldRenderer. The helper constants guarding with typeof value === "object" would throw if value is null. Callers currently avoid this, but the component's onChange(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:1

    This PR changes published packages (packages/core/src/fields/image.ts and packages/core/src/schema/zod-generator.ts, plus packages/admin/src/components/ImageFieldRenderer.tsx), but .changeset/ still contains only README.md and config.json. CONTRIBUTING.md requires a changeset for any published-package behavior change, and both emdash and @emdash-cms/admin are in the fixed release group here so this change will affect the published CHANGELOG.

    Run pnpm changeset and commit the generated Markdown file. Describe the user-visible effect (e.g., "Adds cached filename, mimeType, blurhash, and dominantColor to 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:115

    The selectedFilename, dimensions, and mimeType helpers guard with typeof value === "object", which is also true for null. Current callers in ContentEditor and RepeaterField coerce null to undefined before passing it in, so this doesn't crash today, but the component's own onChange(null) API makes null a natural value for a future caller. Guard against null explicitly:

    	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;
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants