You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Images in translated pages don't render — only the default locale's pages ever went through the image pipeline. In DocsDefinitionResolver.resolve(), the parseImagePaths → upload → replaceImagePathsAndUrls loops iterate parsedDocsConfig.pages, while parsedDocsConfig.translationPages (loaded from fern/translations/<locale>/…) is passed through to FDR untouched, so translated markdown ships its authored relative path instead of a file:<fileId> ref:
Translated pages resolve their <Markdown src> / <Code src> includes afterresolve() returns (in buildTranslatedDocsDefinition.ts, publishDocs.ts, and the preview servers), unlike default-locale pages which resolve includes before image parsing. So the resolver only collects assets — with include tags stripped first so their targets aren't mistaken for images — and the src rewrite happens downstream, after includes are inlined.
A translated page's relative paths are authored from its own location under translations/<locale>/, but tooling that copies the default-locale page verbatim leaves paths that only resolve from the default-locale page's location (Taurus has both shapes: ../../../../assets/… in tr/pt vs ../../assets/… in en). Both semantics are attempted; only references that map to an uploaded file id are substituted, so an unresolved reference keeps exactly what the author wrote.
Changes Made
DocsDefinitionResolver.collectImageFilesInTranslationPages() adds translated pages' on-disk image references to the upload set, without mutating the markdown.
removeMarkdownIncludeTags() / removeCodeIncludeTags() in docs-markdown-utils, applied before parseImagePaths during that collection so include targets are neither uploaded nor rewritten.
replaceImagePathsAndUrlsInTranslatedPage() replaces replaceImagePathsAndUrls in the four translated-page paths (buildTranslatedDocsDefinition, publishDocs, runPreviewServer, runAppPreviewServer), running after include resolution and trying both translated-file- and default-locale-relative path semantics.
Changelog entry under packages/cli/cli/changes/unreleased/.
Testing
Unit tests added/updated — translation-images docs-resolver fixture (en default, tr authoring paths from the translated file plus a translated-only asset, a missing asset, and a <Markdown src> include; pt mirroring the English path) asserting translated-only assets are uploaded and that missing files / include targets are not; parseImagePaths.test.ts covers both path semantics and leaving unresolved references as authored.
pnpm turbo run compile test --filter @fern-api/docs-resolver --filter @fern-api/docs-markdown-utils --filter @fern-api/docs-preview --filter @fern-api/remote-workspace-runner → 53/53 tasks green; pnpm lint:biome --fix and pnpm format:fix clean.
The reason will be displayed to describe this comment to others. Learn more.
🔴 Shared content snippets embedded in translated pages stop appearing
Translated page content is put through the image pipeline (parseImagePathsInTranslationPages at packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:635) before the step that pulls in referenced snippet and code files, so any translated page that embeds a shared snippet loses that content.
Impact: Translated pages that include reusable snippets or external code files render with the include broken/empty, and the snippet files themselves get uploaded as if they were images.
Why running the image pipeline before snippet resolution breaks `` / ``
For default-locale pages the resolver deliberately runs replaceReferencedMarkdown / replaceReferencedCode first (packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:545-582), so by the time parseImagePaths runs there are no <Markdown src="…"/> or <Code src="…"/> tags left.
For translated pages, snippet/code resolution happens afterresolve() returns, in packages/cli/generation/remote-generation/remote-workspace-runner/src/buildTranslatedDocsDefinition.ts:77-91, packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts:1040-1065, and the preview path in packages/cli/docs-preview/src/runAppPreviewServer.ts:878+.
parseImagePaths rewrites the src attribute of any MDX JSX element (packages/cli/docs-markdown-utils/src/parseImagePaths.ts:703-712) with no extension filtering, and adds the resolved path to filepaths. So a translated page containing <Markdown src="/snippets/shared.mdx"/>:
has shared.mdx added to the upload set,
gets its src rewritten to the absolute host path, then
replaceImagePathsAndUrlsInTranslationPages turns it into src="file:snippets/shared.mdx" (the file was uploaded, so it is in collectedFileIds).
Downstream replaceReferencedMarkdown then tries to resolve file:snippets/shared.mdx (it still matches /\.mdx?$/, packages/cli/docs-markdown-utils/src/replaceReferencedMarkdown.ts:108), fails to read it, and the include is never inlined. The same happens to <Code src="./example.py"/>.
Prompt for agents
The new translation image pipeline in DocsDefinitionResolver runs parseImagePaths/replaceImagePathsAndUrls over parsedDocsConfig.translationPages, but snippet and code-reference resolution for translated pages happens later (buildTranslatedDocsDefinition.ts, publishDocs.ts, runAppPreviewServer.ts), unlike default-locale pages where replaceReferencedMarkdown/replaceReferencedCode run before parseImagePaths. Because parseImagePaths rewrites the src attribute of every MDX JSX element (see parseImagePaths.ts around the mdxJsxElement src handling), `<Markdown src="...mdx"/>` and `<Code src="..."/>` in translated pages get rewritten to absolute paths, uploaded as assets, and then converted to `file:<id>` refs, so the later snippet/code inlining can no longer resolve them and the included content is lost.
Possible approaches: run the locale-aware replaceReferencedMarkdown/replaceReferencedCode (and transformAtPrefixImports) for translation pages inside the resolver before the image parse step, mirroring the default-locale ordering, and drop the duplicated processing downstream; or make the translation image parse skip src attributes of Markdown/Code components (or any src pointing at .md/.mdx and code files) until includes have been resolved.
Was this helpful? React with 👍 or 👎 to provide feedback.
The reason will be displayed to describe this comment to others. Learn more.
Fixed: the resolver no longer touches translated markdown. It only collects assets now, and <Markdown src> / <Code src> tags are stripped (removeMarkdownIncludeTags / removeCodeIncludeTags) before parseImagePaths runs, so include targets are never uploaded as assets or rewritten. The actual src rewrite moved downstream into replaceImagePathsAndUrlsInTranslatedPage, called after replaceReferencedMarkdown → replaceReferencedCode → transformAtPrefixImports → stripMdxComments in buildTranslatedDocsDefinition.ts, publishDocs.ts and both preview servers. Covered by the fixture's translated page containing <Markdown src="../../../snippets/shared.mdx" /> (asserted not uploaded).
The reason will be displayed to describe this comment to others. Learn more.
🟡 Local machine file paths can be published in translated pages when an image is missing
When a translated page points at an image file that cannot be found anywhere, the page keeps the machine-local absolute path that was substituted into it (returned at packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:921-925) instead of the author's original reference, so the published page shows a path from the build machine.
Impact: Broken images in translated pages now embed absolute build-machine paths in published docs content instead of the original relative link.
Mechanism: parseImagePaths substitutes absolute paths, and unresolved ones are never mapped back
parseImagePaths rewrites every local src/![]() reference into the resolved absolute filesystem path inside the returned markdown (packages/cli/docs-markdown-utils/src/parseImagePaths.ts:668-712). The fallback branch filters the missing entries out of filepaths (so they are not uploaded) but returns parsed.markdown, which still contains those absolute paths.
Later, replaceImagePathsAndUrls only rewrites paths present in collectedFileIds; for an absolute path with no file id it returns undefined and leaves the text untouched (packages/cli/docs-markdown-utils/src/parseImagePaths.ts:881-899). The downstream second pass in buildTranslatedDocsDefinition.ts cannot recover it either. Before this PR the translated page simply kept the author's relative path.
Prompt for agents
In DocsDefinitionResolver.parseTranslationPageImagePaths, when neither the translated file's location nor the default-locale page's location resolves an image to an existing file, the code warns and drops the path from the upload set but still returns the markdown produced by parseImagePaths, which has already substituted the absolute host filesystem path into the src. Since replaceImagePathsAndUrls leaves unknown absolute paths untouched, that absolute path ends up in the published translated markdown. Consider returning the original (unmodified) markdown for such unresolved references — e.g. by re-substituting the authored relative path back, or by only accepting the parsed markdown when all of its filepaths exist — so the page falls back to the author's original reference rather than leaking a build-machine path.
Was this helpful? React with 👍 or 👎 to provide feedback.
The reason will be displayed to describe this comment to others. Learn more.
Fixed by the same restructuring: translated markdown is no longer rewritten to absolute paths at all. replaceImagePathsAndUrlsInTranslatedPage only substitutes references that resolved to an uploaded file id (trying translated-file-relative then default-locale-relative semantics); anything unresolved keeps the authored relative path. Test: leaves a reference with no uploaded file as authored in parseImagePaths.test.ts, plus assets/missing.png asserted absent from the upload set.
The reason will be displayed to describe this comment to others. Learn more.
🟨 Build-machine absolute filesystem paths can end up in published translated pages
When an image referenced by a translated page cannot be resolved to an existing file, the markdown returned at packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:921-925 still contains the absolute host filesystem path substituted by parseImagePaths (packages/cli/docs-markdown-utils/src/parseImagePaths.ts:668-712). replaceImagePathsAndUrls leaves absolute paths with no uploaded file id untouched (packages/cli/docs-markdown-utils/src/parseImagePaths.ts:881-899), so the published translated page content can disclose local build paths (e.g. CI checkout directories or user home directories).
Was this helpful? React with 👍 or 👎 to provide feedback.
The reason will be displayed to describe this comment to others. Learn more.
Same root cause as the comment above — resolved by moving the rewrite downstream so unresolved references stay as authored instead of being replaced with build-machine absolute paths.
Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-18T04:12:09Z).
Fixture
main
PR
Delta
docs
271.6s (n=5)
255.5s (35 versions)
-16.1s (-5.9%)
Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload). Delta is computed against the nightly baseline on main. Baseline from nightly run(s) on main (latest: 2026-08-18T04:12:09Z). Trigger benchmark-baseline to refresh. Last updated: 2026-08-19 03:47 UTC
Translated page content now goes through the same image parse, upload, and file ID replacement pipeline as default-locale pages.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Images in translated pages don't render — only the default locale's pages ever went through the image pipeline. In
DocsDefinitionResolver.resolve(), theparseImagePaths→ upload →replaceImagePathsAndUrlsloops iterateparsedDocsConfig.pages, whileparsedDocsConfig.translationPages(loaded fromfern/translations/<locale>/…) is passed through to FDR untouched, so translated markdown ships its authored relative path instead of afile:<fileId>ref:Repro: https://taurushq.ferndocs.com/tr/developer-hub (product logos missing; fine on
/developer-hub).Two wrinkles shape where the work happens:
<Markdown src>/<Code src>includes afterresolve()returns (inbuildTranslatedDocsDefinition.ts,publishDocs.ts, and the preview servers), unlike default-locale pages which resolve includes before image parsing. So the resolver only collects assets — with include tags stripped first so their targets aren't mistaken for images — and thesrcrewrite happens downstream, after includes are inlined.translations/<locale>/, but tooling that copies the default-locale page verbatim leaves paths that only resolve from the default-locale page's location (Taurus has both shapes:../../../../assets/…in tr/pt vs../../assets/…in en). Both semantics are attempted; only references that map to an uploaded file id are substituted, so an unresolved reference keeps exactly what the author wrote.Changes Made
DocsDefinitionResolver.collectImageFilesInTranslationPages()adds translated pages' on-disk image references to the upload set, without mutating the markdown.removeMarkdownIncludeTags()/removeCodeIncludeTags()indocs-markdown-utils, applied beforeparseImagePathsduring that collection so include targets are neither uploaded nor rewritten.replaceImagePathsAndUrlsInTranslatedPage()replacesreplaceImagePathsAndUrlsin the four translated-page paths (buildTranslatedDocsDefinition,publishDocs,runPreviewServer,runAppPreviewServer), running after include resolution and trying both translated-file- and default-locale-relative path semantics.packages/cli/cli/changes/unreleased/.Testing
translation-imagesdocs-resolver fixture (en default, tr authoring paths from the translated file plus a translated-only asset, a missing asset, and a<Markdown src>include; pt mirroring the English path) asserting translated-only assets are uploaded and that missing files / include targets are not;parseImagePaths.test.tscovers both path semantics and leaving unresolved references as authored.pnpm turbo run compile test --filter @fern-api/docs-resolver --filter @fern-api/docs-markdown-utils --filter @fern-api/docs-preview --filter @fern-api/remote-workspace-runner→ 53/53 tasks green;pnpm lint:biome --fixandpnpm format:fixclean.Link to Devin session: https://app.devin.ai/sessions/7f7027051e9e4473a340661ea8b77654