Skip to content

fix(cli): process images in translated pages - #17470

Open
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1787108968-translated-page-images
Open

fix(cli): process images in translated pages#17470
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1787108968-translated-page-images

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

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:

<!-- /developer-hub -->     <img src="https://fdr-prod-docs-files-public.s3…/assets/logo-protect.png?…">
<!-- /tr/developer-hub -->  <img src="../../../../assets/logo-protect.png">

Repro: https://taurushq.ferndocs.com/tr/developer-hub (product logos missing; fine on /developer-hub).

Two wrinkles shape where the work happens:

  • Translated pages resolve their <Markdown src> / <Code src> includes after resolve() 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.

Link to Devin session: https://app.devin.ai/sessions/7f7027051e9e4473a340661ea8b77654


Open in Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot 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.

Reviewed the changes — everything looks good. No issues found.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

});
}
}
this.parseImagePathsInTranslationPages(filesToUploadSet);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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 after resolve() 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"/>:

  1. has shared.mdx added to the upload set,
  2. gets its src rewritten to the absolute host path, then
  3. 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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 replaceReferencedMarkdownreplaceReferencedCodetransformAtPrefixImportsstripMdxComments 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).

Comment on lines +921 to +925
return {
...parsed,
filepaths: parsed.filepaths.filter((filepath) => existsSync(filepath)),
metadata: translatedFileMetadata
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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.

Comment on lines +921 to +925
return {
...parsed,
filepaths: parsed.filepaths.filter((filepath) => existsSync(filepath)),
metadata: translatedFileMetadata
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

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

nick-berger-14 and others added 2 commits August 19, 2026 03:34
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>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787108968-translated-page-images branch from ffc7947 to f9f394c Compare August 19, 2026 03:36
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

1 participant