Images Missing When Exporting Markdown to Word? How We Solved Cross-Origin and WebP Compatibility Issues
While building the Markdown-to-Word export feature at md-to.com, we hit a frustrating bug: images in users’ Markdown content vanished after exporting to Word.
The root cause turned out to involve two layers: browser security policy (CORS) and image format compatibility (WebP). This post documents the debugging process and our final solution.
The Symptom
A user pastes Markdown like this into the editor:


The preview renders both images just fine. But in the exported Word file, both images are gone — replaced by placeholder text like [Editor interface] and [Site icon].
Problem 1: Word Doesn’t Support WebP
Root Cause
We use the docx library to generate Word documents. Its ImageRun component only supports four image formats:
type DocxImageType = 'jpg' | 'png' | 'gif' | 'bmp';
Modern websites heavily use WebP (25-35% smaller than PNG). When our code encountered an unsupported format, it threw an Unsupported image type error and the image was silently dropped.
Solution: Auto-Convert via Canvas
Browsers natively render WebP, so we can use the Canvas API to convert it to PNG:
function loadImageViaCanvas(
source: Blob | string,
): Promise<{ blob: Blob; width: number; height: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
const isBlobSource = source instanceof Blob;
if (!isBlobSource) {
img.crossOrigin = 'anonymous';
}
const blobUrl = isBlobSource ? URL.createObjectURL(source) : null;
img.onload = () => {
if (blobUrl) URL.revokeObjectURL(blobUrl);
const { naturalWidth: width, naturalHeight: height } = img;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
canvas.toBlob(
(pngBlob) => {
if (pngBlob) resolve({ blob: pngBlob, width, height });
else reject(new Error('Canvas toBlob failed'));
},
'image/png',
);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = blobUrl ?? (source as string);
});
}
This function accepts two types of input:
- Blob: for pure format conversion of already-fetched images
- URL string: for CORS fallback (more on this below)
Regardless of input format (WebP, AVIF, etc.), the output is always PNG — which Word handles perfectly.
Problem 2: CORS Blocks Cross-Origin Image Fetching
After fixing the WebP issue, we discovered a more fundamental problem: cross-origin images couldn’t be fetched at all.
Root Cause
Our code uses fetch() to get image data:
const response = await fetch(imageUrl);
const blob = await response.blob();
When the image URL is on a different domain than the website (e.g., site is md-to.com, image is on example.com), the browser’s CORS (Cross-Origin Resource Sharing) policy blocks the request:
Access to fetch at 'https://example.com/image.png'
from origin 'https://md-to.com' has been blocked by CORS policy
Why Does Preview Work but Export Doesn’t?
Because the browser applies different security levels to different operations:
| Operation | Cross-origin allowed? | Why |
|---|---|---|
<img src="external"> rendering | ✅ Yes | Browser renders images without CORS |
fetch() data access | ❌ Blocked | JS reading binary data requires CORS |
<img crossOrigin> + Canvas export | ❌ Blocked | Tainted canvas cannot export data |
fetch({mode: 'no-cors'}) | ❌ Unreadable | Returns empty body |
The preview uses <img> tags (cross-origin rendering allowed), while export needs JavaScript to read the image’s binary data (blocked by CORS). That’s why you can “see it but can’t export it.”
Why Base64 Doesn’t Help Either
You might think: “Just convert images to Base64!”
But Base64 is just an encoding format, not a data acquisition method. The problem isn’t “what format to store in” — it’s “the browser won’t let you access the raw bytes.” Whether you want to encode as Base64, ArrayBuffer, or anything else, the first step is getting the data — and CORS blocks that completely.
Solution: Server-Side Image Proxy
Since there’s no browser-side workaround, we let the server fetch the images. Servers have no CORS restrictions and can freely request any URL.
We created a lightweight Edge Function as an image proxy:
Browser → /api/image-proxy?url=https://example.com/image.png → Our server → example.com
↑ No CORS restriction
Proxy function (deployed at the edge):
export async function onRequest({ request }) {
const { searchParams } = new URL(request.url);
const target = searchParams.get('url');
// Security: only http(s), only image types, size limit
const upstream = await fetch(target);
const contentType = upstream.headers.get('content-type');
return new Response(await upstream.arrayBuffer(), {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
'Access-Control-Allow-Origin': '*',
},
});
}
Client-side auto-routing:
function resolveImageSrc(src: string): string {
const resolved = new URL(src, window.location.href).toString();
// Route cross-origin images through proxy
if (new URL(resolved).origin !== window.location.origin) {
return `/api/image-proxy?url=${encodeURIComponent(resolved)}`;
}
return resolved;
}
All cross-origin image requests now become same-origin requests — no more CORS issues.
Final Architecture
Image URL in Markdown
│
▼
resolveImageSrc()
│
├── Same-origin → Direct fetch
│
└── Cross-origin → /api/image-proxy?url=xxx
│
▼
Edge Function forwards
│
▼
Got Blob
│
▼
getDocxImageType() check format
│
┌─────────┴──────────┐
▼ ▼
jpg/png/gif/bmp webp/avif/other
Embed directly Canvas → PNG → Embed
Development Environment
A matching proxy middleware was added to the Vite dev server, ensuring consistent behavior between development and production.
Security Measures
The image proxy is not an unrestricted open proxy:
- Only allows
http://andhttps://protocols - Only passes through image Content-Types (png/jpeg/gif/bmp/webp/avif/svg)
- 10 MB size limit per image
- 24-hour cache on responses
Summary
| Problem | Cause | Solution |
|---|---|---|
| WebP images don’t appear in Word | docx library doesn’t support WebP | Canvas API converts to PNG |
| Cross-origin images can’t be fetched | Browser CORS security policy | Server-side Edge Function proxy |
| Images fail in dev environment too | localhost → external domain is cross-origin | Vite dev server proxy middleware |
Key takeaway: The browser’s security model distinguishes between “rendering” and “data access.” Just because <img> can display a cross-origin image doesn’t mean JavaScript can read its data. When you need client-side access to cross-origin resources, a server-side proxy is virtually the only reliable approach.
Want to try it out? Visit md-to.com Markdown to Word, paste some Markdown with external images, and export.
Related Tools
Convert MD to Word (Docx) Online - Free Markdown to Word
Convert Markdown to Word documents instantly. Free online MD to DOCX converter with real-time preview. Keep formatting intact. No signup required - try now!
Free Word to Markdown Converter - Convert DOCX to MD Online
Convert Word documents to Markdown instantly. Free online DOCX to MD converter preserves headings, tables, lists, and formatting. No signup required - try now!
Free Markdown to LaTeX Converter - Convert MD to TeX Online
Convert Markdown to LaTeX online for free. Transform your MD documents to TeX format for academic papers and technical documents. No registration required!