feat: wire up site-wide default Open Graph image#997
Conversation
Adds end-to-end support for the previously-unused seo.defaultOgImage
site setting:
- Resolves the media reference in getSiteSettings (matches logo/favicon)
- Emits og:image, twitter:image, and BlogPosting JSON-LD image from
EmDashHead.astro when a page has no image of its own
- Absolutizes the URL using SiteSettings.url, page.siteUrl, or the
request origin so crawlers and JSON-LD consumers that reject relative
URLs work correctly
- Adds a Default Social Image picker to admin SEO settings with preview,
change, remove, and orphaned-reference state
Adds a localOnly prop to MediaPickerModal that suppresses the Insert
from URL input, external provider tabs, and the providers fetch. Logo,
favicon, and the new default OG image picker all use it since the
storage shape only persists a local mediaId; URL/provider selections
would round-trip to unresolvable references.
Wires site-settings cache invalidation across every media mutation path:
runtime handlers (REST + MCP), plugin context media.delete(), and the
local-runtime provider delete. Resolved logo/favicon/defaultOgImage
URLs and dimensions stay in sync with the underlying media row.
Splits the Zod media reference schema into mediaReferenceInput and
mediaReferenceResponse so generated OpenAPI clients see the resolved
fields on read while writes still strip them.
Adds a /// <reference types="astro/client" /> directive to query.ts so
the dynamic await import("astro:content") typechecks under any
consumer's tsconfig.
🦋 Changeset detectedLatest commit: ba640c3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-perf-coordinator | ba640c3 | May 12 2026, 07:05 AM |
Scope checkThis PR changes 1,196 lines across 20 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-i18n | ba640c3 | May 12 2026, 07:05 AM |
|
/review |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | ba640c3 | May 12 2026, 07:06 AM |
PR template validation failedPlease fix the following issues by editing your PR description:
See CONTRIBUTING.md for the full contribution policy. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | ba640c3 | May 12 2026, 07:06 AM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@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-forms
@emdash-cms/plugin-webhook-notifier
commit: |
|
UnknownError: "Overloaded" |
|
@ascorbic Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | ba640c3 | May 12 2026, 07:07 AM |
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. |
There was a problem hiding this comment.
Pull request overview
This PR wires up the previously-defined but non-functional seo.defaultOgImage site setting end-to-end, ensuring the default social image is resolved from the media table, emitted into SEO output when a page has no image, and kept consistent via cache invalidation on media mutations.
Changes:
- Resolve
seo.defaultOgImagemedia references in site settings reads, and expose resolved fields (url,contentType,width,height) in response schemas. - Emit
og:image,twitter:image, and BlogPosting JSON-LDimageusing a site-wide fallback, with absolute-URL enforcement and SSRF/invalid-scheme guards. - Add a
localOnlymode to the admin media picker and a Default Social Image picker UI (plus orphan-reference UX) for site settings.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/tests/unit/settings/settings.test.ts | Adds regression + invalidation tests for resolving seo.defaultOgImage and cache busting on media mutations. |
| packages/core/tests/unit/plugins/page-seo.test.ts | Tests fallback precedence and JSON-LD propagation for the site default OG image. |
| packages/core/tests/unit/page/absolute-url.test.ts | Adds comprehensive unit tests for origin resolution and URL absolutization/guards. |
| packages/core/src/settings/types.ts | Updates MediaReference docs and clarifies input vs response schema shapes. |
| packages/core/src/settings/index.ts | Resolves seo.defaultOgImage in getSiteSettingWithDb and getSiteSettingsWithDb. |
| packages/core/src/query.ts | Adds an astro/client types reference to improve typechecking for dynamic astro:content imports. |
| packages/core/src/plugins/context.ts | Invalidates site settings cache when plugins delete media. |
| packages/core/src/page/seo-contributions.ts | Adds optional defaultOgImage fallback into base SEO tags and passes through to JSON-LD builder. |
| packages/core/src/page/jsonld.ts | Adds optional default-image fallback to BlogPosting JSON-LD generation. |
| packages/core/src/page/absolute-url.ts | Introduces helpers for safe absolutization of media URLs for crawler-facing markup. |
| packages/core/src/media/local-runtime.ts | Invalidates site settings cache on local provider deletes. |
| packages/core/src/emdash-runtime.ts | Invalidates site settings cache on successful media update/delete via runtime handlers. |
| packages/core/src/components/EmDashHead.astro | Fetches site settings default OG image, absolutizes it, and feeds it into base SEO generation. |
| packages/core/src/astro/routes/api/media/[id].ts | Documents that cache invalidation occurs in EmDashRuntime.handleMediaDelete. |
| packages/core/src/api/schemas/settings.ts | Splits media reference schema into input vs response, exposing resolved fields in responses. |
| packages/admin/tests/components/MediaPickerModal.test.tsx | Adds coverage for localOnly behavior (no URL input, no provider tabs, no provider fetch). |
| packages/admin/src/components/settings/SeoSettings.tsx | Adds Default Social Image picker UI with preview and orphan-reference handling. |
| packages/admin/src/components/settings/GeneralSettings.tsx | Applies orphan-reference UX to logo/favicon and switches “configured” gating to mediaId. |
| packages/admin/src/components/MediaPickerModal.tsx | Implements localOnly prop to suppress URL insert + external providers and skip provider fetches. |
| .changeset/site-default-og-image.md | Changeset documenting the new feature and related behavior changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
/review |
|
OK, the route accepts arbitrary keys via I've reviewed all major surfaces. Let me also quickly check the I think this PR is solid. Looking through everything:
Let me check one thing that nagged me — the WHITESPACE_OR_CONTROL_RE is at module scope and called for every absolutize. That's fine. I'm done. No bugs found that warrant a blocking comment. Per the skill rules: "Silence means approval." LGTM! |
What does this PR do?
Wires up the previously-unused `seo.defaultOgImage` site setting end-to-end. The schema, MCP tool descriptors, and type definitions all referenced this field, but stored values were never resolved and no template path read them. Setting it in the admin UI silently did nothing.
This PR closes that loop:
Along the way:
Closes #
Type of change
Checklist
AI-generated code disclosure
Screenshots / test output
```
Test Files 205 passed (205) Tests 3257 passed (3257) [core]
Test Files 62 passed (62) Tests 867 passed (867) [admin]
```
Notable test coverage: