feat(markdown): render images in DocsUI::Markdown
Merged
MH
14d ago+178+178+012345678901234567890123456789-4-4-0123456789
Summary
 in an md island rendered nothing: DocsUI::Markdown had no node_image handler, so the visitor fell back to visit_children and left only the alt text as stray paragraph text. It now renders a lazy-loaded <img>.
| Source | src |
|---|---|
relative (dir/shot.png) | image_path → /assets/dir/shot-<digest>.png when a view context is present; verbatim in an isolated render |
https://…, //…, /… | verbatim |
javascript:, data:, any other non-http(s) scheme (also JaVaScRiPt: and java<TAB>script:) | no <img> |
altis the description's text and inline-code content, with markup dropped (→alt="Emphasised alt"). Phlex escapes it.- An image title becomes
title=. Classes:my-4 rounded-box border border-base-300(IMAGE_CLASSES), all already in the kit's Tailwind vocabulary. - The
.mdtwin already exported<img>as(MarkdownExport::Inline#image). A new spec covers the full round trip. - The handler lives in a new
DocsUI::MarkdownImagesmixin (app/components/docs_ui/markdown_images.rb), included byDocsUI::Markdown. /docs/markdowngains an Images example that renders the site's ownog/og.png.
No new API or config. Backwards compatible: Markdown without images renders exactly as before.
Closes #94
Test plan
-
spec/docs_ui/markdown_spec.rb: relative src throughimage_path(stubbed view-context seam, as inmeta_tags_spec); verbatim with no view context; absolute, protocol-relative and root-relative kept as written; alt built from nested emphasis and code;title; alt escaping; unsafejavascript:, mixed-case,data:,vbscript:and control-character schemes render no<img> -
spec/docs_kit/markdown_export_spec.rb:DocsUI::Markdownimage →.mdtwin -
bundle exec rspec: 1066 examples, 0 failures, line coverage 93.0% -
bundle exec rubocopon the changed files: clean -
docs/bin/rspec: 15 examples, 0 failures - Booted dogfood app:
/docs/markdownrenders<img src="/assets/og/og-9a6baa07.png" …>, the asset returns 200, and/docs/markdown.mdcontains
Before / after (/docs/markdown, Images example)
Before, only the alt text "The docs-kit landing page" appears as a paragraph. After, the image renders:
Deviations & judgment calls
- Discovery:
node_linkhas no scheme guard —[x](javascript:alert(1))renders a livejavascript:href today (commonmarker keeps the raw URL in the AST; only its HTML renderer sanitises). Left out of scope; worth a follow-up issue. - Judgment call: the scheme check strips ASCII whitespace/control characters first (
java\tscript:is a scheme to a browser) and drops every non-http(s) scheme (vbscript:,file:,blob:), not onlyjavascript:/data:. An unsafe image renders nothing, alt included — no stray alt prose. - Judgment call:
IMAGE_CLASSES = "my-4 rounded-box border border-base-300"— the Code block's box look plus Prose's vertical rhythm. Nomax-w-fulladded: Tailwind preflight already setsimg { max-width: 100% }. - Judgment call: alt includes inline-code text (
→alt="x"), not just text nodes, so a code word isn't silently lost. - Deviation: the issue's spec list didn't ask for a dogfood example; added an
## Imagesexample to/docs/markdownusing the site's existingog/og.png, so the feature is visible and screenshot-able. - Deviation: the issue put the helpers in
markdown.rb; they live in a newDocsUI::MarkdownImagesmixin (app/components/docs_ui/markdown_images.rb) becauseDocsUI::Markdownalready sat at RuboCop's 100-lineMetrics/ClassLengthlimit. One unrelated line was also shortened (node_paragraphbecame an endless def) to get the class back to 100. I chose that over adding a ClassLength exclude. - Discovery: whole-repo
bundle exec rubocopinspects 0 files when run from a.claude/worktrees/*checkout (the.rubocop.ymlexcludes.claude/**), so agents in worktrees must lint the changed files by name. I verified it that way. - Verified in a booted host: in the dogfood app,
og/og.pngrenders as/assets/og/og-9a6baa07.png, the asset returns 200, and/docs/markdown.mdcontains.
Summary by cubic
Fixes Markdown images rendering nothing in md islands:  now renders a lazy-loaded <img> instead of leaving only the alt text as stray paragraph prose.
- Relative
srcpaths resolve through the asset pipeline (image_path) with a view context, and are used verbatim in isolated renders; absolute, protocol-relative, and root-relative URLs are kept as written. javascript:,data:, and other non-http(s) schemes — including ones hidden behind whitespace or control characters — render no image.- The alt is the description's text and inline code with markup dropped, and a word boundary turns line breaks into spaces; a title becomes the
titleattribute. - The handler lives in a new
DocsUI::MarkdownImagesmixin soDocsUI::Markdownstays under the class-length limit; the.mdexport round-trips the image back to. /docs/markdowngains an Images example using the site'sog/og.png.
Closes #94.