marked instance.
Content · 13 of 13
A fixed-width card whose body is CMS markdown rendered to HTML at
component scope, with a custom marked renderer supplying the
Tailwind classes.
| Prop | Type | Notes |
|---|---|---|
| item * | PainPoint | The two fields the component reads, declared in the component itself: image.url, image.name, content (markdown). A generated CMS entity satisfies it unchanged; the component carries no CMS schema. |
| resolveImage | (url: string) => string | Supplied by the caller for relative media URLs; omit it when they are already absolute. |
The renderer overrides two node types only:
paragraph → <p class="text-lg"> and
list → <ul|ol class="pt-2 pl-5 list-disc">.
Everything else falls through to marked's defaults, unstyled.
Fixed upstream — v-html is now sanitized.
This card used to parse item.content with marked and
bind the result straight to v-html. Markdown permits raw HTML and
marked dropped its own sanitize option in v5, so
anything an editor could type into the CMS content field
executed in every visitor's page — an XSS sink gated only by CMS access
control. As of the 2026-08-20 resync the component wraps the parse in
sanitize() from isomorphic-dompurify, which closes it.
Kept here rather than deleted because the specimen is the record of what the
component was, and this was the site's only P0.
The sanitizer here is real.
The storybook resolves isomorphic-dompurify to
kitchen-sink/ports/sanitizer.adapter.ts, which is plain
dompurify at the version the isomorphic wrapper itself
resolves to. The wrapper's only job is pairing DOMPurify with
jsdom so the call also works during SSR; a docs page is
always a browser, so the port drops the half that has nothing to do here
and keeps the engine. Same defaults as the site — no config is passed
there either. The third story above is the proof rather than this
paragraph.
Markdown is parsed once, outside any reactive scope.
htmlContent is computed at <script setup> body
level from props.item.content, not in a computed.
If the parent swaps item without remounting, the card keeps
rendering the previous body while the image updates — the two halves of the
card can disagree.
The ordered-list branch still gets list-disc.
The renderer picks ol vs ul from
token.ordered but applies the same class string either way, so a
numbered list renders with bullets and no numbers.
Width is fixed at w-56.
14rem regardless of viewport or content, with only the horizontal margin
responding to breakpoints. Long copy grows the card downward indefinitely,
which is what the carousel parent expects — but it means the component
cannot be reused in a wider slot.