diff --git a/.changeset/quiet-rivers-bloom.md b/.changeset/quiet-rivers-bloom.md new file mode 100644 index 000000000..a29a5ac73 --- /dev/null +++ b/.changeset/quiet-rivers-bloom.md @@ -0,0 +1,5 @@ +--- +"emdash": minor +--- + +Renders local media through storage `publicUrl` when configured. `EmDashImage` and the Portable Text image block now call a new `locals.emdash.getPublicMediaUrl()` helper, so R2 and S3 deployments with a custom domain serve images from that domain. `S3Storage.getPublicUrl` now returns the `/_emdash/api/media/file/{key}` path when no `publicUrl` is set (previously `{endpoint}/{bucket}/{key}`). diff --git a/packages/core/src/astro/middleware.ts b/packages/core/src/astro/middleware.ts index 315e9c469..939064bf6 100644 --- a/packages/core/src/astro/middleware.ts +++ b/packages/core/src/astro/middleware.ts @@ -43,6 +43,7 @@ import { } from "../emdash-runtime.js"; import { setI18nConfig } from "../i18n/config.js"; import type { Database, Storage } from "../index.js"; +import { createPublicMediaUrlResolver } from "../media/url.js"; import type { SandboxRunner } from "../plugins/sandbox/types.js"; import type { ResolvedPlugin } from "../plugins/types.js"; import { getRequestContext, runWithContext } from "../request-context.js"; @@ -301,10 +302,11 @@ export const onRequest = defineMiddleware(async (context, next) => { try { const runtime = await getRuntime(config, initSubTimings); setupVerified = true; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for these two methods + // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods locals.emdash = { collectPageMetadata: runtime.collectPageMetadata.bind(runtime), collectPageFragments: runtime.collectPageFragments.bind(runtime), + getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage), } as EmDashHandlers; } catch { // Non-fatal — EmDashHead will fall back to base SEO contributions @@ -445,6 +447,7 @@ export const onRequest = defineMiddleware(async (context, next) => { // Direct access (for advanced use cases) storage: runtime.storage, db: runtime.db, + getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage), hooks: runtime.hooks, email: runtime.email, configuredPlugins: runtime.configuredPlugins, diff --git a/packages/core/src/astro/types.ts b/packages/core/src/astro/types.ts index 9ec810b7a..083503155 100644 --- a/packages/core/src/astro/types.ts +++ b/packages/core/src/astro/types.ts @@ -339,6 +339,7 @@ export interface EmDashHandlers { // Direct access to storage and database for advanced use cases storage: import("../index.js").Storage | null; db: Kysely; + getPublicMediaUrl?: (storageKey: string) => string; // Hook pipeline for plugin integrations hooks: import("../plugins/hooks.js").HookPipeline; diff --git a/packages/core/src/components/EmDashImage.astro b/packages/core/src/components/EmDashImage.astro index 88c26a35f..710c58974 100644 --- a/packages/core/src/components/EmDashImage.astro +++ b/packages/core/src/components/EmDashImage.astro @@ -20,6 +20,7 @@ import type { MediaValue } from "../fields/types.js"; import type { HTMLAttributes } from "astro/types"; import type { ImageEmbed } from "../media/types.js"; import { getMediaProvider } from "../media/provider-loader.js"; +import { buildRenderMediaUrl } from "../media/url.js"; // Standard responsive breakpoints const BREAKPOINTS = [640, 750, 828, 960, 1080, 1280, 1600, 1920]; @@ -53,14 +54,14 @@ function normalizeImage( } /** - * Build the URL for a local image + * Build the URL for a local image. Prefers `meta.storageKey`; falls back to + * the internal proxy with `img.id` when no storage key is available. */ function buildLocalImageUrl(img: MediaValue): string { - const storageKey = (img.meta?.storageKey as string) || img.id; - if (storageKey) { - return `/_emdash/api/media/file/${storageKey}`; - } - return ""; + return buildRenderMediaUrl(Astro.locals.emdash?.getPublicMediaUrl, { + storageKey: img.meta?.storageKey as string | undefined, + id: img.id, + }); } /** diff --git a/packages/core/src/components/Gallery.astro b/packages/core/src/components/Gallery.astro index 7d2bf5b3a..01a89594b 100644 --- a/packages/core/src/components/Gallery.astro +++ b/packages/core/src/components/Gallery.astro @@ -6,6 +6,7 @@ * Uses Astro's Image component for optimization when dimensions are available. */ import { Image as AstroImage } from "astro:assets"; +import { buildRenderMediaUrl } from "../media/url.js"; export interface Props { node: { @@ -39,9 +40,10 @@ if (!images.length) {