Skip to content

fix(seo): buildMediaUrl handles root-relative paths without doubling the API prefix#1167

Open
abhishekshankar wants to merge 2 commits into
emdash-cms:mainfrom
abhishekshankar:fix/buildmediaurl-root-relative
Open

fix(seo): buildMediaUrl handles root-relative paths without doubling the API prefix#1167
abhishekshankar wants to merge 2 commits into
emdash-cms:mainfrom
abhishekshankar:fix/buildmediaurl-root-relative

Conversation

@abhishekshankar
Copy link
Copy Markdown

Summary

buildMediaUrl(imageRef, siteUrl) in packages/core/src/seo/index.ts currently handles two cases:

  1. Absolute URL (https://...) → returned as-is
  2. Anything else → treated as a bare media_id and built as ${siteUrl}/_emdash/api/media/file/${imageRef}

But the CMS SEO panel stores seo_image as a root-relative path that already includes the API prefix, e.g. /_emdash/api/media/file/01KS...svg. Branch 2 fires and produces:

https://example.com/_emdash/api/media/file//_emdash/api/media/file/01KS.svg
                                          ^^ doubled

…which 404s and breaks <meta property="og:image"> for every post that used the SEO panel to set an image.

Repro

import { getSeoMeta } from "emdash/seo";

const meta = getSeoMeta(
  { seo: { image: "/_emdash/api/media/file/01KS.svg" }, data: {} },
  { siteUrl: "https://example.com" },
);

console.log(meta.ogImage);
// → "https://example.com/_emdash/api/media/file//_emdash/api/media/file/01KS.svg"
// expected:
// → "https://example.com/_emdash/api/media/file/01KS.svg"

Fix

Add a third branch BEFORE the bare-media_id branch that detects root-relative inputs (imageRef.startsWith("/")) and joins them with the host directly — no API-prefix re-prepending.

if (imageRef.startsWith("/")) {
  return siteUrl
    ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${imageRef}`
    : imageRef;
}

Test plan

New unit tests at packages/core/tests/unit/seo/get-seo-meta.test.ts cover:

  • ✅ Absolute URLs pass through unchanged
  • ✅ Root-relative paths join with siteUrl without doubling the prefix
  • ✅ Root-relative paths returned as-is when no siteUrl
  • ✅ Bare media_id still builds the full API path
  • ✅ Trailing slash on siteUrl is stripped before joining

These don't need DB setup — direct unit tests against getSeoMeta.

Bonus (separate PR if you'd like)

The same logic gets re-implemented by user code that needs to resolve media references outside of getSeoMeta (avatar URLs, JSON-LD ImageObject.url, list-page thumbnails). Those sites currently either reach for import { buildMediaUrl } from "emdash/seo" (fails — not exported) or re-implement the logic inline and drift.

Exporting buildMediaUrl would be a one-line addition:

-export { getContentSeo, getSeoMeta };
+export { buildMediaUrl, getContentSeo, getSeoMeta };

Happy to file as a follow-up.

Reference

We currently carry this fix as a patch-package patch in a downstream fork: https://github.com/abhishekshankar/abhishek-shankar.com-emdash/blob/master/patches/emdash%2B0.9.0.patch — happy to delete it once this lands.

…API prefix

`buildMediaUrl(imageRef, siteUrl)` currently handles two cases:

  1. Absolute URL → returned as-is
  2. Anything else → treated as a bare media_id and built as
     `${siteUrl}/_emdash/api/media/file/${imageRef}`

But the CMS SEO panel (and several other code paths) stores
`seo_image` as a root-relative path that ALREADY includes the API
prefix, e.g. `/_emdash/api/media/file/01KS....svg`. The current code
falls into branch 2 and produces:

  https://example.com/_emdash/api/media/file//_emdash/api/media/file/01KS.svg

which 404s, breaking <meta property="og:image"> for every post that
used the SEO panel to set an image.

Repro:

  import { getSeoMeta } from "emdash/seo";
  const meta = getSeoMeta(
    { seo: { image: "/_emdash/api/media/file/01KS.svg" }, data: {} },
    { siteUrl: "https://example.com" },
  );
  console.log(meta.ogImage);
  // before: https://example.com/_emdash/api/media/file//_emdash/api/media/file/01KS.svg
  // after:  https://example.com/_emdash/api/media/file/01KS.svg

Fix: add a third branch BEFORE the bare-media_id branch that detects
root-relative paths (`imageRef.startsWith("/")`) and just joins with
the host. Tests cover all three branches plus the no-siteUrl case
and the trailing-slash siteUrl case.

Tests live at packages/core/tests/unit/seo/get-seo-meta.test.ts —
lightweight unit tests, no DB setup needed.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 25, 2026

⚠️ No Changeset found

Latest commit: 62eb8d4

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
Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

  • This PR does not use the required PR template. Please edit the description to use the PR template. Copy it into your PR description and fill out all sections.

See CONTRIBUTING.md for the full contribution policy.

@github-actions
Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


Abhishek Shankar seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label May 30, 2026
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels May 31, 2026
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a correct, well-scoped fix for a real bug.

The root cause is confirmed: SeoImageField.tsx stores local OG images as root-relative paths like /_emdash/api/media/file/{storageKey}. buildMediaUrl previously only distinguished absolute URLs from bare media IDs, so those root-relative paths fell into the bare-ID branch and got the API prefix prepended a second time, producing a 404 URL.

The fix adds an early startsWith("/") branch that joins the path directly with siteUrl (or returns it as-is when no siteUrl is provided). The logic is sound and matches the existing absolutizeMediaUrl helper in page/absolute-url.ts.

The new unit tests in packages/core/tests/unit/seo/get-seo-meta.test.ts cover the regression and relevant edge cases: absolute URLs, root-relative with/without siteUrl, bare media IDs, and trailing-slash stripping. They mirror the source structure and will be picked up by the existing vitest config (tests/**/*.test.ts).

One AGENTS.md convention is missing: this PR changes the published emdash package and therefore needs a changeset in .changeset/.

}

// Build from media API path
// Root-relative path — the CMS SEO panel stores seo_image as
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.

[needs fixing] This PR changes the published emdash package but does not include a .changeset/*.md file. AGENTS.md requires a changeset whenever a published package changes.

Please run pnpm changeset and follow the prompts to add a patch-level changeset describing the fix.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR cla: needed review/awaiting-author Reviewed; waiting on the author to respond size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants