Summary
On Portuguese (/pt/) and Spanish (/es/) translated documentation pages, images render as raw text [Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/...] instead of actual <img> elements. English pages display images correctly.
Example broken page: /pt/docs/understanding-comapeos-core-concepts-and-functions
Symptom
| Locale |
Markdown in file |
Rendered |
| English |
 |
✅ image |
| Portuguese |
[Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/c1033c29-9030-4781-...] |
❌ linked text |
The embedded S3 URLs are expired (X-Amz-Expires=3600), so they cannot be recovered from the URLs alone.
Affected files (confirmed)
i18n/pt/docusaurus-plugin-content-docs/current/understanding-comapeos-core-concepts-and-functions.md (line 32)
i18n/pt/docusaurus-plugin-content-docs/current/troubleshooting/teste-ttulo-da-pgina-no-menu-e-nas-migalhas-de-navegao.md
i18n/pt/docusaurus-plugin-content-docs/current/getting-started-essentials/initial-use-and-comapeo-settings.md
- Additional ES files likely affected (not yet fully enumerated)
Root Cause: Two compounding pipeline failures
Failure 1 — markdownToNotion.ts deliberately degrades images to paragraph text
File: scripts/notion-translate/markdownToNotion.ts lines 340–361, 453–464, 739–748
When notion-translate pushes the translated page to Notion, the image case handler converts every markdown image node into a paragraph block containing literal [Image: url] text:
case "image": {
// For translations, we'll just convert images to text to avoid Notion API issues
notionBlocks.push({
paragraph: {
rich_text: [{ type: "text", text: { content: `[Image: ${altText || imageUrl}]` } }],
},
});
}
For pages translated before image stabilization was added, imageUrl was the raw S3 presigned URL (which expires in 1 hour).
For newer translations, imageUrl is /images/file.png — the correct local path — but it is still stored as paragraph text, not as a Notion image block.
Failure 2 — notion-fetch-all overwrites the correct i18n/ files
saveTranslatedContentToDisk (index.ts line 1222) does write correct  files to i18n/pt/ immediately after translation. However, when notion-fetch-all or notion-fetch-auto-translation-children subsequently re-fetches all Notion pages, it overwrites those files with content read back from the Notion database — and those Notion PT pages now have [Image: url] paragraph blocks (not image blocks). The custom imageTransformer in notionClient.ts (lines 341–438) only fires for blocks of type === "image", so it never triggers on these paragraphs. processAndReplaceImages also cannot match them (its regex only matches  syntax). The broken text is written verbatim to disk.
Corruption Sequence
┌─────────────────────────────────────────────────────────────────────────────────┐
│ notion:translate runs │
│ │
│ Notion EN page │
│ [image block: s3-url] ──► n2m imageTransformer ──►  │
│ │ │
│ processAndReplaceImages │
│ downloads → static/images/file.png │
│ │ │
│ markdownContent:  │
│ │ │
│ translateText (OpenAI) ──► translatedContent:  │
│ │ │
│ ┌────────────┴────────────────────────────────┐ │
│ ▼ (A) saveTranslatedContentToDisk ▼ (B) createNotionPageWithBlocks │
│ i18n/pt/file.md ✅ CORRECT (temporary) markdownToNotion.ts converts: │
│  paragraph: "[Image: /images/file.png]" │
│ pushed to Notion PT page ❌ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ notion-fetch-all runs LATER (overwrites i18n/pt/) │
│ │
│ Notion PT page │
│ [paragraph: "[Image: /images/file.png]"] │
│ │ │
│ ▼ n2m renders paragraph as plain text │
│ imageTransformer NOT triggered (block type ≠ "image") │
│ processAndReplaceImages: no  pattern found │
│ │ │
│ ▼ │
│ i18n/pt/file.md ❌ ← OVERWRITES the correct file from step (A) │
│ [Image: /images/file.png] │
│ (or expired S3 URL for older translations) │
└─────────────────────────────────────────────────────────────────────────────────┘
Key files involved
| File |
Lines |
Role in bug |
scripts/notion-translate/markdownToNotion.ts |
340–361, 453–464, 739–748 |
Converts image nodes → [Image: url] paragraph text when uploading to Notion |
scripts/notion-translate/translateBlocks.ts |
159–217 |
Converts Notion image blocks → callout blocks with 🖼️ icon; never re-converted back to images on fetch |
scripts/notion-fetch/imageReplacer.ts |
105–289 |
extractImageMatches regex only matches  — [Image: url] plain text is invisible to it |
scripts/notion-translate/index.ts |
1054–1069, 1222 |
Correct files written to disk by saveTranslatedContentToDisk, but overwritten by subsequent fetch runs |
scripts/notionClient.ts |
341–438 |
Custom imageTransformer only fires for Notion image blocks; paragraph blocks with [Image: url] text bypass it |
docusaurus.config.ts |
290 |
remarkFixImagePaths applies to all docs incl. i18n, but only fixes images/ → /images/ prefix — cannot recover [Image: url] text |
Design intent vs. actual behavior
Intent: All locales reference the same static/images/ files. Translated markdown uses  — only text changes, not images.
Actual: The Notion round-trip (translate → push to Notion → re-fetch) loses image information, because markdownToNotion.ts converts images to unrecoverable paragraph text.
Proposed next steps
Short-term: Post-process existing broken i18n files
Write a script (scripts/fix-i18n-images.ts) that:
- Scans
i18n/pt/ and i18n/es/ for files containing [Image: ...] patterns
- For each broken file, finds the corresponding English doc in
docs/
- Extracts image references in order from the English file (
)
- Replaces each
[Image: ...] occurrence (in ordinal position) with the matching English image reference
This works because static images are shared across all locales — no per-locale image download needed.
Long-term: Fix the pipeline to prevent recurrence
Two options:
- Option A: Make
generateBlocks.ts / notionClient.ts recognize the [Image: /images/file.png] paragraph pattern and convert it back to proper  markdown when fetching translated pages.
- Option B: Instead of writing image-as-text to Notion, store translated image blocks as Notion callout blocks with a recognizable sentinel (e.g.
🖼️ emoji prefix + local path) and teach notion-fetch to convert these sentinels back to image markdown.
- Option C: Make
notion-fetch-all skip overwriting i18n/ files where image references are already correct, or make saveTranslatedContentToDisk the authoritative source (run it after, not before, the Notion upload).
The translateBlocks.ts callout approach (Option B, partially implemented) is promising but currently the callout is not converted back to an image on fetch — that conversion step is missing.
Acceptance criteria
Summary
On Portuguese (
/pt/) and Spanish (/es/) translated documentation pages, images render as raw text[Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/...]instead of actual<img>elements. English pages display images correctly.Example broken page:
/pt/docs/understanding-comapeos-core-concepts-and-functionsSymptom
[Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/c1033c29-9030-4781-...]The embedded S3 URLs are expired (
X-Amz-Expires=3600), so they cannot be recovered from the URLs alone.Affected files (confirmed)
i18n/pt/docusaurus-plugin-content-docs/current/understanding-comapeos-core-concepts-and-functions.md(line 32)i18n/pt/docusaurus-plugin-content-docs/current/troubleshooting/teste-ttulo-da-pgina-no-menu-e-nas-migalhas-de-navegao.mdi18n/pt/docusaurus-plugin-content-docs/current/getting-started-essentials/initial-use-and-comapeo-settings.mdRoot Cause: Two compounding pipeline failures
Failure 1 —
markdownToNotion.tsdeliberately degrades images to paragraph textFile:
scripts/notion-translate/markdownToNotion.tslines 340–361, 453–464, 739–748When
notion-translatepushes the translated page to Notion, the image case handler converts every markdown image node into a paragraph block containing literal[Image: url]text:For pages translated before image stabilization was added,
imageUrlwas the raw S3 presigned URL (which expires in 1 hour).For newer translations,
imageUrlis/images/file.png— the correct local path — but it is still stored as paragraph text, not as a Notion image block.Failure 2 —
notion-fetch-alloverwrites the correcti18n/filessaveTranslatedContentToDisk(index.ts line 1222) does write correctfiles toi18n/pt/immediately after translation. However, whennotion-fetch-allornotion-fetch-auto-translation-childrensubsequently re-fetches all Notion pages, it overwrites those files with content read back from the Notion database — and those Notion PT pages now have[Image: url]paragraph blocks (notimageblocks). The customimageTransformerinnotionClient.ts(lines 341–438) only fires for blocks oftype === "image", so it never triggers on these paragraphs.processAndReplaceImagesalso cannot match them (its regex only matchessyntax). The broken text is written verbatim to disk.Corruption Sequence
Key files involved
scripts/notion-translate/markdownToNotion.ts[Image: url]paragraph text when uploading to Notionscripts/notion-translate/translateBlocks.tsimageblocks →calloutblocks with 🖼️ icon; never re-converted back to images on fetchscripts/notion-fetch/imageReplacer.tsextractImageMatchesregex only matches—[Image: url]plain text is invisible to itscripts/notion-translate/index.tssaveTranslatedContentToDisk, but overwritten by subsequent fetch runsscripts/notionClient.tsimageTransformeronly fires for Notionimageblocks; paragraph blocks with[Image: url]text bypass itdocusaurus.config.tsremarkFixImagePathsapplies to all docs incl. i18n, but only fixesimages/→/images/prefix — cannot recover[Image: url]textDesign intent vs. actual behavior
Intent: All locales reference the same
static/images/files. Translated markdown uses— only text changes, not images.Actual: The Notion round-trip (translate → push to Notion → re-fetch) loses image information, because
markdownToNotion.tsconverts images to unrecoverable paragraph text.Proposed next steps
Short-term: Post-process existing broken i18n files
Write a script (
scripts/fix-i18n-images.ts) that:i18n/pt/andi18n/es/for files containing[Image: ...]patternsdocs/)[Image: ...]occurrence (in ordinal position) with the matching English image referenceThis works because static images are shared across all locales — no per-locale image download needed.
Long-term: Fix the pipeline to prevent recurrence
Two options:
generateBlocks.ts/notionClient.tsrecognize the[Image: /images/file.png]paragraph pattern and convert it back to propermarkdown when fetching translated pages.🖼️emoji prefix + local path) and teachnotion-fetchto convert these sentinels back to image markdown.notion-fetch-allskip overwritingi18n/files where image references are already correct, or makesaveTranslatedContentToDiskthe authoritative source (run it after, not before, the Notion upload).The
translateBlocks.tscallout approach (Option B, partially implemented) is promising but currently the callout is not converted back to an image on fetch — that conversion step is missing.Acceptance criteria
/pt/docs/understanding-comapeos-core-concepts-and-functionsdisplays images correctly[Image: ...]text in any file underi18n/notion-fetch-allafter anotion:translatedoes not re-introduce broken image text