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.

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.

1 participant