Skip to content

Fix .md endpoint serving "[object Object]" for externalSource-backed pages#29

Merged
pjb157 merged 1 commit into
mainfrom
claude/dreamy-shannon-p31omf
Jun 16, 2026
Merged

Fix .md endpoint serving "[object Object]" for externalSource-backed pages#29
pjb157 merged 1 commit into
mainfrom
claude/dreamy-shannon-p31omf

Conversation

@pjb157

@pjb157 pjb157 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

curl https://docs.doubleword.ai/inference-api/async-agents.md (also pr-review-bot.md, cli-examples.md) returns HTTP 200 with the literal body [object Object]. These are Sanity docPages whose real content lives in the top-level externalSource field (raw GitHub README URLs). Per AGENTS.md, AI agents rely on the .md endpoints, so this is user-facing breakage.

Root cause

Two bugs in src/app/api/markdown/[product]/[...slug]/route.ts:

  1. The route's GROQ query only selected linkedPost->{body, externalSource} — never the docPage's own externalSource — so the handler never fetched the external README and fell through to doc.body.
  2. doc.body for these pages is a placeholder Portable Text block array, not a markdown string. Passing the array to new NextResponse(...) stringifies it (the fetch spec's ToString conversion), producing [object Object].

The HTML page route (src/app/[product]/[...slug]/page.tsx) already resolves content as externalSource > linkedPost > body and coerces Portable Text — the markdown route had drifted from it.

Fix

  • Select externalSource in the markdown route's query and resolve content in the same order as the page route: externalSourcelinkedPost.externalSourcelinkedPost.bodybody.
  • Mirror the page route's HTML guard: if externalSource returns text/html (pages that use it only to power the "View source" link), fall back to the Sanity body instead of serving HTML.
  • Extract coerceMarkdownContent (Portable Text → markdown) from MarkdownRenderer.tsx into shared src/lib/portable-text.ts and use it in the route, so the endpoint can never emit a stringified object regardless of body shape.
  • New: optional SANITY_API_HOST env override on the Sanity client (no-op unless set, documented in AGENTS.md) so a local mock Sanity can back offline dev/verification in sandboxed environments that can't reach *.sanity.io.

Tests

  • route.test.ts: externalSource page serves fetched markdown; failed external fetch falls back to coerced Portable Text (asserts no [object Object]); HTML externalSource falls back to body; plain string body + image rewriting unchanged; linkedPost.externalSource path unchanged; 404 for unknown docs; .md suffix stripping.
  • portable-text.test.ts: coercion contract (strings pass through, blocks render, non-strings never stringify to [object Object]).

Verification

Ran npm run dev against a local mock Sanity API (via SANITY_API_HOST) returning the async-agents doc (Portable Text placeholder body + real externalSource URL) and a normal string-body page:

  • Before (original route): curl localhost:3000/inference-api/async-agents.md → HTTP 200, body [object Object] — reproduces the production symptom exactly.
  • After: same curl → HTTP 200, 12,371 bytes of the actual README fetched from raw.githubusercontent.com/doublewordai/use-cases/.../async-agents/README.md ("# Async Agents: Deep Research in a Day for $0.34 …").
  • Normal Sanity-body page (quickstart.md) serves its markdown string unchanged before and after.

vitest run (51 tests, only pre-existing search.test.ts > stripMarkdown failure, which also fails on a clean checkout of main), tsc --noEmit clean, eslint introduces no new findings. The Vercel preview build for this PR passed (deployment Ready), which covers npm run build (not runnable in this sandbox — no Sanity network access).

Preview spot-check (sandbox can't reach *.vercel.app, so please click — each should be markdown, not [object Object]):

https://claude.ai/code/session_017nS4EWobkNNbu1jCGZ92LL

The raw-markdown endpoint returned a literal "[object Object]" (HTTP
200) for docPages whose content comes from the top-level
externalSource field — e.g. /inference-api/async-agents.md. The
route's GROQ query never selected externalSource, so the handler fell
through to the page's placeholder Portable Text body, and NextResponse
stringified the block array.

- select externalSource and resolve content in the same order as the
  HTML page route: externalSource > linkedPost > body
- ignore HTML responses from externalSource (repo-link pages), falling
  back to the Sanity body, matching the page route
- coerce Portable Text bodies to markdown via coerceMarkdownContent,
  extracted from MarkdownRenderer into src/lib/portable-text.ts, so
  the endpoint can never emit a stringified object
- add tests for the route handler and the coercion helper
- support a SANITY_API_HOST override in the Sanity client so a local
  mock can back offline dev/verification (documented in AGENTS.md)

https://claude.ai/code/session_017nS4EWobkNNbu1jCGZ92LL
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
documentation Ready Ready Preview, Comment Jun 11, 2026 2:16pm

Request Review

@pjb157 pjb157 marked this pull request as ready for review June 16, 2026 13:18
Copilot AI review requested due to automatic review settings June 16, 2026 13:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes the .md markdown API route so Sanity docPages backed by externalSource return actual markdown (instead of stringifying Portable Text to "[object Object]"), and keeps markdown/body resolution consistent with the HTML doc page route.

Changes:

  • Update the markdown route GROQ query + resolver order to support docPage.externalSource, with an HTML-response guard for external fetches.
  • Extract Portable Text → markdown coercion into a shared helper and use it in both the renderer and the markdown API route.
  • Add targeted Vitest coverage for the markdown API route and the Portable Text coercion contract; document SANITY_API_HOST and add an optional client override for offline/local mocking.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/sanity/lib/client.ts Adds optional SANITY_API_HOST override for pointing the Sanity client at a stand-in API host.
src/lib/portable-text.ts New shared coerceMarkdownContent helper to safely convert Portable Text blocks to markdown strings.
src/lib/portable-text.test.ts Unit tests for coercion behavior (strings passthrough, blocks rendered, no "[object Object]").
src/components/MarkdownRenderer.tsx Switches renderer to use shared coercion helper (removes duplicated implementation).
src/app/api/markdown/[product]/[...slug]/route.ts Fixes .md endpoint content resolution to handle externalSource and Portable Text bodies.
src/app/api/markdown/[product]/[...slug]/route.test.ts Adds route-level tests for externalSource fetch, fallbacks, HTML guard, image rewriting, 404, and .md stripping.
AGENTS.md Documents SANITY_API_HOST for offline dev/verification workflows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +106 to +121
// Get the raw markdown content, resolved in the same order as the HTML
// page route: externalSource > linkedPost > body
let externalContent: string | null = null;
if (doc.externalSource) {
externalContent = await fetchExternalContent(doc.externalSource);
} else if (doc.linkedPost?.externalSource) {
externalContent = await fetchExternalContent(doc.linkedPost.externalSource);
}

// Sanity bodies may be Portable Text blocks rather than markdown strings;
// coerce so the response is always markdown, never a stringified object
let content =
externalContent ??
(coerceMarkdownContent(doc.linkedPost?.body) ||
coerceMarkdownContent(doc.body));

@pjb157 pjb157 merged commit 0f2b867 into main Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants