diff --git a/.changeset/add-og-image-field.md b/.changeset/add-og-image-field.md new file mode 100644 index 0000000..003e2a9 --- /dev/null +++ b/.changeset/add-og-image-field.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/admin": patch +--- + +Add OG Image field to content editor diff --git a/packages/admin/src/components/ContentEditor.tsx b/packages/admin/src/components/ContentEditor.tsx index 42e5ab7..1122e0a 100644 --- a/packages/admin/src/components/ContentEditor.tsx +++ b/packages/admin/src/components/ContentEditor.tsx @@ -66,6 +66,7 @@ import { } from "./PortableTextEditor"; import { RevisionHistory } from "./RevisionHistory"; import { SaveButton } from "./SaveButton"; +import { SeoImageField } from "./SeoImageField"; import { SeoPanel } from "./SeoPanel"; import { TaxonomySidebar } from "./TaxonomySidebar"; @@ -613,25 +614,46 @@ export function ContentEditor({ )} >
- {Object.entries(fields).map(([name, field]) => ( - - ))} + {Object.entries(fields).map(([name, field]) => { + const fieldEl = ( + + ); + if ( + name === "featured_image" && + field.kind === "image" && + hasSeo && + !isNew && + onSeoChange + ) { + return ( +
+
{fieldEl}
+
+ +
+
+ ); + } + return fieldEl; + })}
@@ -1138,6 +1160,11 @@ function FieldRenderer({ return ( void; required?: boolean; } -function ImageFieldRenderer({ label, value, onChange, required }: ImageFieldRendererProps) { +function ImageFieldRenderer({ + label, + description, + value, + onChange, + required, +}: ImageFieldRendererProps) { const [pickerOpen, setPickerOpen] = React.useState(false); // Normalize value to get display URL (handles both object and legacy string) // Prefer previewUrl for admin display, fall back to src, then derive from storageKey/id @@ -1265,6 +1299,7 @@ function ImageFieldRenderer({ label, value, onChange, required }: ImageFieldRend mimeTypeFilter="image/" title={`Select ${label}`} /> + {description &&

{description}

} {required && !displayUrl && (

This field is required

)} diff --git a/packages/admin/src/components/SeoImageField.tsx b/packages/admin/src/components/SeoImageField.tsx new file mode 100644 index 0000000..69f5609 --- /dev/null +++ b/packages/admin/src/components/SeoImageField.tsx @@ -0,0 +1,84 @@ +/** + * SEO OG Image field for the content editor. + * + * Renders an image picker (reusing MediaPickerModal) that stores the + * selected image URL in `seo.image`. Designed to sit next to the + * Featured Image field in a two-column grid. + */ + +import { Button, Label } from "@cloudflare/kumo"; +import { Image as ImageIcon, X } from "@phosphor-icons/react"; +import * as React from "react"; + +import type { ContentSeo, ContentSeoInput, MediaItem } from "../lib/api"; +import { MediaPickerModal } from "./MediaPickerModal"; + +export interface SeoImageFieldProps { + seo?: ContentSeo; + onChange: (seo: ContentSeoInput) => void; +} + +export function SeoImageField({ seo, onChange }: SeoImageFieldProps) { + const [pickerOpen, setPickerOpen] = React.useState(false); + const imageUrl = seo?.image || null; + + const handleSelect = (item: MediaItem) => { + const isLocalProvider = !item.provider || item.provider === "local"; + const url = isLocalProvider + ? `/_emdash/api/media/file/${item.storageKey || item.id}` + : item.url; + onChange({ image: url }); + }; + + const handleRemove = () => { + onChange({ image: null }); + }; + + return ( +
+ + {imageUrl ? ( +
+ +
+ + +
+
+ ) : ( + + )} +

+ Image shown when this page is shared on social media +

+ +
+ ); +}