Markdown Image Url Resolution
DL#161 — Markdown Image URL Resolution
Written for AI agents. See Log Methodology Note below for details.
Background
The markdown plugin (@jay-framework/markdown) renders .md files via the markdown-pages component. When markdown content contains relative image references like , the rendered HTML produces <img src="22%20-%20flow.svg"> relative to the page URL — but no static file exists at that route. The dev server interprets it as a markdown page request and fails:
GET /design-log/22%20-%20serialized%20mutable%20flow%202.svg
Error: ENOENT: no such file or directory, open 'content/22 - serialized mutable flow 2.svg.md'
The images sit alongside the .md files in the content directory, but that directory isn't served as static files.
Problem
- Relative image paths in markdown resolve against the page URL, not the content directory
- The content directory isn't exposed as a public-accessible path
- No mechanism to rewrite image URLs to externally hosted versions (e.g., CDN)
Design
Three layers. Layers 1+2 work together as the zero-config default. Layer 3 replaces both when provided.
Layer 1: Rewrite relative URLs to content directory path
In the markdown parser, intercept <img src="..."> tags. If src is relative (not http:// or https://), rewrite it to be served from the public folder where layer 2 copies the files.
Use marked's renderer.image override with an imageBaseUrl computed from the content directory's public-relative path.
Layer 2: Copy media files to public directory (default)
During loadParams (build time), scan the content directory for non-.md files (images, SVGs, diagrams) and copy them to the project's public/ folder under a matching path. The public folder path is read from the .jay config (devServer.publicFolder, defaults to ./public).
This is the default behavior — no props needed. Images alongside markdown files are automatically made available as static assets.
contentDir: design-log/content/
public folder: ./public/
Copy: design-log/content/22 - flow.svg
→ public/design-log/content/22 - flow.svg
URL rewrite: <img src="/design-log/content/22%20-%20flow.svg">
Layer 3: Media mapping file (replaces layers 1+2)
Optional mediaMap prop pointing to a YAML file that maps local filenames to CDN URLs with responsive variants:
# media-map.yaml — generated by an upload script
'22 - flow.svg':
width: 800
height: 600
src: 'https://static.wixstatic.com/media/abc123/v1/fill/w_800,h_600/flow.webp'
srcset:
- url: 'https://static.wixstatic.com/media/abc123/v1/fill/w_400,h_300/flow.webp'
width: 400
- url: 'https://static.wixstatic.com/media/abc123/v1/fill/w_800,h_600/flow.webp'
width: 800
'diagram.png':
width: 1200
height: 900
src: 'https://static.wixstatic.com/media/def456/v1/fill/w_1200,h_900/diagram.webp'
srcset:
- url: 'https://static.wixstatic.com/media/def456/v1/fill/w_400,h_300/diagram.webp'
width: 400
- url: 'https://static.wixstatic.com/media/def456/v1/fill/w_800,h_600/diagram.webp'
width: 800
- url: 'https://static.wixstatic.com/media/def456/v1/fill/w_1200,h_900/diagram.webp'
width: 1200
<script type="application/jay-headless" ...>
contentDir: ./content
mediaMap: ./media-map.yaml
</script>
When mediaMap is provided:
- Mapped images use the external URL — no file copying, no local serving
- Unmapped images fall back to layers 1+2
widthandheightfrom the mapping are added to the<img>tag (prevents CLS)srcsetvariants produce responsive<img>withsrcsetandsizesattributes
Rendered output for a mapped image with srcset:
<img
src="https://cdn.../w_800/flow.webp"
srcset="
https://cdn.../w_400/flow.webp 400w,
https://cdn.../w_800/flow.webp 800w,
https://cdn.../w_1200/flow.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px"
width="800"
height="600"
loading="lazy"
alt="Diagram"
/>
Without srcset (just src + dimensions):
<img src="https://cdn.../w_800/flow.webp" width="800" height="600" loading="lazy" alt="Diagram" />
Upload script responsibilities
The upload script is external tooling (not part of the markdown plugin). It:
- Scans the content directory for image files
- Reads original dimensions (e.g., using
sharporimage-size) - Uploads to CDN (e.g., wix-media API) requesting responsive variants at configured breakpoints
- Writes/updates the
media-map.yamlfile
The script decides what responsive widths to generate. Typical breakpoints: [400, 800, 1200]. For each image, it generates variants up to the original width (no upscaling).
Input configuration (script-specific, not part of the plugin):
# upload-config.yaml (example)
breakpoints: [400, 800, 1200]
format: webp
quality: 85
The markdown plugin only consumes the mapping file — it doesn't know or care how images were uploaded or what CDN is used.
Implementation Plan
Phase 1: Layers 1+2 (zero-config)
parse-markdown.ts— addimageBaseUrl?: stringoption tocreateMarkedParser(). Overriderenderer.imageto prepend the base URL to relativesrcvalues.markdown-pages.ts— inloadParams, scan contentDir for non-.mdfiles and copy to public folder (read path from.jayconfig or default./public). ComputeimageBaseUrlfrom the content directory's public-relative path. Pass to parser inwithSlowlyRender.
No new props needed. Works out of the box.
Phase 2: Layer 3 (media mapping with srcset)
parse-markdown.ts— acceptmediaMapoption with type:
InRecord< string, { src: string; width?: number; height?: number; srcset?: Array<{ url: string; width: number }>; } >;renderer.image, check map first. If srcset entries exist, render responsive<img>withsrcsetandsizes. Fall back to base URL for unmapped images.markdown-pages.ts— ifmediaMapprop is set, read and parse the YAML file, pass to parser. Skip file copying for mapped images.- Contract — add optional
mediaMapprop.
Phase 3: Agent-kit documentation
agent-kit/designer/markdown-usage.md— document:- Default behavior (images auto-copied to public/, no config needed)
mediaMapprop for CDN hosting- Mapping file format (src, width, height, srcset)
- Example with wix-media URLs
- Note that upload scripts are external tooling
Trade-offs
| Choice | Pro | Con |
|---|---|---|
| Zero-config default (copy to public/) | Works immediately, no setup needed | Duplicates files on disk |
| Mapping file replaces local copy | CDN-optimized, responsive images | Requires external upload script |
| srcset in mapping file | Responsive images from markdown | Script must generate multiple variants |
sizes attribute generation |
Browser picks optimal image | Default sizes may not match all layouts |
Read .jay config for public folder |
Consistent with project setup | Component depends on project config |
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.