Skip to content

feat: wire up site-wide default Open Graph image#997

Merged
ascorbic merged 1 commit into
mainfrom
feat/site-default-og-image
May 12, 2026
Merged

feat: wire up site-wide default Open Graph image#997
ascorbic merged 1 commit into
mainfrom
feat/site-default-og-image

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

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:

  • Resolves the media reference in `getSiteSettings` so the URL, contentType, and dimensions are populated alongside `mediaId` (matches the existing `logo`/`favicon` pattern).
  • Emits `og:image`, `twitter:image`, and BlogPosting JSON-LD `image` from `EmDashHead.astro` whenever a page has no image of its own. Per-page images still take precedence.
  • 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 an orphaned-reference state (when the underlying media row was deleted, the Remove button stays visible so users can clear the dangling `mediaId`).

Along the way:

  • 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`/`seo.defaultOgImage` URLs and dimensions now stay in sync with the underlying media row.
  • Splits the Zod media reference schema into `mediaReferenceInput` (write, just `mediaId` + `alt`) and `mediaReferenceResponse` (read, includes resolved fields) so generated OpenAPI clients see the resolved fields without write paths having to defensively strip them.
  • Applies the same orphan-ref UI to logo and favicon — previously a stranded provider/URL-derived reference would render "Select Logo" and silently re-save the dangling `mediaId` on any unrelated change.
  • Adds a `/// <reference types="astro/client" />` directive to `query.ts` so the dynamic `await import("astro:content")` typechecks under any consumer's tsconfig. The directive is stripped from compiled output, and every `emdash` consumer is an Astro site that already provides these declarations.

Closes #

Type of change

  • Feature

Checklist

  • I have read CONTRIBUTING.md
  • `pnpm typecheck` passes
  • `pnpm lint` passes (39 baseline warnings, no new ones)
  • `pnpm test` passes (3257 core tests, 867 admin tests; +37 new)
  • `pnpm format` has been run
  • I have added/updated tests for my changes
  • User-visible strings in the admin UI are wrapped for translation. No `messages.po` changes included; the post-merge workflow will extract them.
  • I have added a changeset — see `.changeset/site-default-og-image.md` (minor bump for `emdash` and `@emdash-cms/admin`).
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7 (OpenCode). Reviewed across five rounds with a GPT-5.5 second-opinion sub-agent; 21 issues found and fixed before opening.

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:

  • `packages/core/tests/unit/page/absolute-url.test.ts` — 32 cases covering precedence, scheme allowlist, SSRF guards (protocol-relative, control chars, header-injection vectors), and opaque-origin rejection.
  • `packages/core/tests/unit/settings/settings.test.ts` — resolution of `seo.defaultOgImage` via both `getSiteSettings()` and `getSiteSetting("seo")`, and end-to-end invalidation via `EmDashRuntime.handleMediaUpdate`/`handleMediaDelete`.
  • `packages/core/tests/unit/plugins/page-seo.test.ts` — fallback precedence (`seo.ogImage > page.image > defaultOgImage`), JSON-LD propagation, Twitter card upgrade, no-emission when neither is set.
  • `packages/admin/tests/components/MediaPickerModal.test.tsx` — `localOnly` suppresses URL input, external provider tabs, and the providers fetch (with a control assertion proving providers DO appear without the prop).

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.
Copilot AI review requested due to automatic review settings May 12, 2026 07:05
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: ba640c3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@cloudflare-workers-and-pages
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-perf-coordinator ba640c3 May 12 2026, 07:05 AM

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

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

@cloudflare-workers-and-pages
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-i18n ba640c3 May 12 2026, 07:05 AM

@ascorbic
Copy link
Copy Markdown
Collaborator Author

/review

@cloudflare-workers-and-pages
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
docs ba640c3 May 12 2026, 07:06 AM

@github-actions
Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

See CONTRIBUTING.md for the full contribution policy.

@cloudflare-workers-and-pages
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 ba640c3 May 12 2026, 07:06 AM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: ba640c3

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 12, 2026

UnknownError: "Overloaded"

github run

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 12, 2026

@ascorbic Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@cloudflare-workers-and-pages
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 ba640c3 May 12 2026, 07:07 AM

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.defaultOgImage media references in site settings reads, and expose resolved fields (url, contentType, width, height) in response schemas.
  • Emit og:image, twitter:image, and BlogPosting JSON-LD image using a site-wide fallback, with absolute-URL enforcement and SSRF/invalid-scheme guards.
  • Add a localOnly mode 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.

@ascorbic
Copy link
Copy Markdown
Collaborator Author

/review

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 12, 2026

OK, the route accepts arbitrary keys via [...key]. Not a concern.

I've reviewed all major surfaces. Let me also quickly check the astro/routes/api/media/[id].ts change — it's only a comment change, no functional difference.

I think this PR is solid. Looking through everything:

  1. Logic: Fallback precedence (seo.ogImage > page.image > defaultOgImage) is correctly implemented and tested.
  2. Edge cases: Empty refs, orphaned refs, missing media — all handled. Whitespace/control/scheme guards comprehensive.
  3. Error handling: try/catch in resolveMediaReference; degradation by returning bare ref.
  4. State/concurrency: Worker-scoped cache invalidation wired at all three media mutation entry points.
  5. Security: SSRF guard via protocol-relative rejection, scheme allowlist, opaque origin rejection, whitespace/control char filter. The whitespace regex includes the \r\n header injection vector. Good.
  6. Data integrity: Zod schema split prevents resolved-field write-back; strip mode is default.
  7. Resource management: No new resource concerns.

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!

github run

@ascorbic ascorbic merged commit 7b45cba into main May 12, 2026
39 of 40 checks passed
@ascorbic ascorbic deleted the feat/site-default-og-image branch May 12, 2026 08:34
@emdashbot emdashbot Bot mentioned this pull request May 12, 2026
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