Skip to content

feat: Decode /_og/d/** route using full catch-all segment (avoid losing props when %2F is decoded to /) #522

@matej-marjanovic

Description

@matej-marjanovic

🆒 Your use case

Summary

nuxt-og-image builds dynamic OG image URLs under /_og/d/<encoded-options>.<ext>, where <encoded-options> is a comma-separated, encodeURIComponent-encoded payload (including component props such as coverSrc for site-relative paths like /img/foo/bar.jpg).

Problem: Some environments and tools (notably Vercel’s deployment “Open Graph” preview, and other crawlers/debuggers) normalize or display the same logical URL with literal / characters where the wire format used %2F. After that normalization, the HTTP path is split into multiple segments between /d/ and the final .png.

The module’s server handler currently derives the encoded options string with logic equivalent to:

  • take pathname from the request
  • path.split('/').pop() to get the “filename” segment
  • strip the extension and decodeOgImageParams(...)

When unencoded / characters appear inside what should be a single logical segment (because %2F was decoded), .pop() returns only the last fragment. The decoded options then omit most props (component, headline, subheadline, coverSrc, etc.), while fragments that happen to live in the last segment (e.g. the p / _path base64 chunk) may still parse. The generated image becomes a partial/broken card (e.g. badge + author + site name only), even though the canonical og:image URL in HTML (with %2F) works.

This creates inconsistent previews between:

  • fetching the exact og:image string from HTML, and
  • tools that rewrite the URL with decoded slashes before requesting the image.

Reproduction (conceptual)

  1. Define an OG image with a prop whose encoded form contains %2F (typical for a site-relative file path in a prop such as coverSrc, e.g. /img/blog/post/cover.jpg).
  2. Confirm the page’s og:image meta URL works when requested as emitted (percent-encoded).
  3. Request the same logical URL after replacing %2F with / in the path (simulating a UI or fetcher that decodes path segments).
  4. Observe that the image response may correspond to truncated/partial decode options (missing headline, wrong template props, missing hero image), or otherwise diverges from (2).

🆕 The solution you'd like

Suggested direction (optimal fix)

Preferred: For the Nitro route /_og/d/**, resolve the encoded options from the full catch-all match (the entire remainder of the path after /_og/d/), including internal / characters, up to the final .<extension>, instead of using only pathname.split('/').pop().

Concretely, the handler should reconstruct one string:

encodedSegment = <everything after /_og/d/ without the leading slash, with path segments joined, until the file extension>

or equivalently read the raw matched ** segment from the router and strip .png (etc.) from the end.

That way:

  • Requests with %2F (single path segment in the URI) continue to work as today.
  • Requests where intermediaries have turned %2F into / still map to the same decoded options as long as the server receives the full path string (which is the usual case for /_og/d/**).

Alternative / additional hardening: If relying on raw path parsing remains fragile across hosts, the module could document or support passing path-like props only in non-slash encodings (e.g. optional base64url field) — but that pushes complexity to users; fixing segment extraction is the more general solution.

🔍 Alternatives you've considered

Workaround (today)

Consumers can avoid / inside individual prop values (e.g. encode site paths as base64url in coverSrc and decode in the template). That works but is non-obvious and duplicates logic in every project that uses path-like OG props.

Why this is worth fixing upstream

  • Social / SEO tooling should be able to fetch og:image URLs that are equivalent under normal URI normalization without changing semantics.
  • Developer experience: Path-shaped props are natural for hero/cover images; users should not need custom encoding solely because some platforms decode %2F.

ℹ️ Additional info

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions