Marp Expert
Convert content into Marp slide decks that render cleanly via marp --pdf --allow-local-files. The constraints below are non-obvious pitfalls confirmed against marpit.marp.app — read once, apply always.
Hard rules
These break rendering silently if violated:
- Only
for images. Marp does NOT support![w:60% center],![width:200px], or any inline image sizing. If you need an image, it must be a background. - Wrap paths containing spaces in
<...>:. Without the brackets, marp silently treats the space as URL terminator. - Inline SVG requires
--html. Marp escapes HTML by default, so a<svg>diagram renders as a page of visible source code. Any deck containing inline SVG must be rendered withmarp deck.marp.md --html --pdf --allow-local-files. See "Smart drawing" below. - Slides overflow when content is too dense. Marp has no auto-shrink. Split proactively.
- Max 15 visible lines per slide. Count every rendered line — paragraph lines, bullet items, table rows (including the header row), code block lines, quote/callout-box lines. Over 15 and the bottom of the slide gets cut off in the PDF. See "15-line hard cap" below.
先分清:页级版式 vs 图级版式
这是两种完全不同的东西,用错工具是最常见的返工原因:
| 页级版式(slide layout) | 图级版式(diagram) | |
|---|---|---|
| 排的是 | 一页的内容块——文字、表格、图 | 一张图内部的节点和连线 |
| 手段 | CSS grid + <div> / marp 语法 |
内联 <svg> |
要不要 --html |
不需要 | 需要 |
| 文件 | references/layout-patterns.md | references/infographics-svg/ |
| 例子 | 三栏对比、双栏各带表、上下分区 | 阶梯、箭头串、2×2、桑基、泳道、便当格 |
判断:这一页要表达「内容之间的结构关系」吗?
- 是(要看谁依赖谁、怎么演进、卡在哪)→ 画图
- 否(只是内容多,并排放不下)→ 排版式
Layout patterns (cheat sheet)
| Scenario | Pattern |
|---|---|
| Cover | <!-- _class: cover --> + # Title + ## Subtitle |
| Major section break | <!-- _class: divider --> + # Section Name |
| Text only / text + table | Plain markdown, default content slide |
| Two columns | <div class="cols cols-2"> — 并列论点、两套方案 |
| Three columns | <div class="cols cols-3"> — 演进/挑战/对策,每栏 ≤ 5 行 |
| Main + side | <div class="cols cols-main"> (2:1) — 左要点右佐证,最常用 |
| Top / bottom split | <div class="split-h"> — 上结论下证据 |
| Single image + text |  — image left, text right |
| Multi-image, no text |   — horizontal row by default |
| Text + 2 images |  +  — img(a) right column, img(b) fills left, content overlays left |
| Portrait multi-image | Add vertical to each:  etc. |
| Infographic / 关系图 | <!-- _class: diagram --> + inline <svg> — see below |
15-line cap is per slide, not per column — a 3-column slide gives each column only ~5 lines. Splitting into three pages usually reads better than cramming one.
For full syntax and orientation logic, read references/image-syntax.md. For column layout details and the CSS block, read references/layout-patterns.md.
Infographics & relation diagrams — 内联 SVG
Infographics and relation diagrams (staircase, arrow chain, 2×2 matrix, value tree, sankey, funnel, swimlanes, bento grid, argument map, pie, radar, balance wheel) are drawn as inline SVG written directly in the .md — not as images. A PNG has to be redrawn to change one number; SVG travels with the file, keeps its text searchable, stays vector in the PDF, and recolours with one hex value.
# 含内联 SVG 的 deck 必须带 --html,否则 SVG 被转义成一整页源码
marp deck.marp.md --html --pdf --allow-local-files
The diagram module is a four-layer pipeline. Work through them in order — each answers exactly one question:
| Layer | Question | File |
|---|---|---|
| ① structures 选题 | 这份内容能画哪几张图? | structures.md |
| ② skeletons 体裁 | 用什么图形承载这个关系? | skeletons/INDEX.md |
| ③ metaphors 语气 | 在同一副骨架上换什么叙事外壳? | metaphors.md |
| ④ styles 配色 | 什么颜色、什么字阶? | styles/INDEX.md |
| craft 笔法 | 画布多大?线怎么连?字怎么排? | craft/ |
Pick the skeleton in two steps: relationship first, then metaphors. Relationship families (hierarchy / sequence / cycle / comparison / matrix / framework / strategy / mapping / growth / composition / profile / balance) route to a skeleton. The narrative layer — staircase, pyramid, flywheel, iceberg, funnel, bridge, mountain — swaps the shell on that same skeleton without changing the routing. That separation is what lets the skeleton library grow without the metaphors table ever changing.
| Relationship | Answers | Skeleton |
|---|---|---|
| Sequence | what happens first, then next | Arrow chain |
| Hierarchy (depth 3+) | where value comes from | Value tree |
| Matrix (2 independent dimensions) | who gets the money, who doesn't | 2×2 quadrant |
| Growth / evolution | where we are, what's next | Staircase |
| Flow with magnitude | where does the volume go | Sankey |
| Parallel tracks | who did what, when | Swimlanes |
| Argument | what backs this claim | Toulmin map |
| Composition | part-to-whole shares, 1–3 pies | Pie / donut |
| Profile | multi-dimension score shape | Radar |
| Balance | one object, current vs target | Balance wheel |
Non-negotiables when writing SVG into markdown:
- No blank lines inside the SVG block — markdown cuts the HTML block at the first blank line and the diagram vanishes without an error.
- Unique
marker/clipPathids per diagram (a1,a2, …) — in a merged single-HTML export every SVG shares one DOM, so duplicate ids make all arrowheads resolve to the first definition. - No
---inside the SVG — it is still parsed as a page break. - Font sizes come from CSS classes, never a
font-sizeattribute — the deck'sstyle:block already definessvg .t / .tb / .sm / .qt / .lbl(see references/style-bootstrap.md), so one edit re-sizes every diagram. - Add
<!-- _class: diagram -->when diagram content reaches the bottom of the viewBox (axis titles, legends, footnotes) — the CSS hides the footer on those slides so they don't collide. - Keep nodes ≤ 7 per diagram. Past that, split the page or fall back to a table.
- One colour family + one accent. Three or more hues turn muddy under projection.
- Geometry must not contradict the data. A funnel drawn layer-by-layer with drifting slopes reads as a stack of plates, and casual band widths imply proportions the numbers don't support. Draw one generatrix through all bands, add rim + spout, and label the diagram "band width is schematic" — exact proportions belong in a sankey. See flow-cycle.
Before shipping, run both gates — the first catches what is certainly wrong, the second catches what merely looks wrong:
node scripts/svg-lint.mjs deck.marp.md # 6 deterministic checks
marp deck.marp.md --html --images png -o check # render and actually look
Trust the rendered PNG, never the SVG source.
Full templates with coordinate formulas, connector-semantics table, visual-hierarchy rules, and QA checklist: references/infographics-svg/. Every skeleton, metaphors and style, one per slide: examples/infographic-gallery.marp.md.
Content density — the 15-line hard cap
Every slide renders at most 15 lines of content. Count them before shipping. HTML comments are excluded.
Count each of these as one line:
| Element | How to count |
|---|---|
| Paragraph | One visual line as wrapped, ≈ 38 CJK chars or ≈ 75 Latin chars per line at default font. A 4-sentence paragraph ≈ 3 lines. |
| Bullet / numbered item | 1 line per item, plus 1 more if it wraps |
| Table | 1 line per row, including the header row. A 4-column × 4-row table = 5 lines. |
| Code block | 1 line per code line — no exceptions, no partial credit |
Quote / tip / warn box (>) |
Every wrapped line inside the box |
| Heading | Count it — ## costs 1 line before the body starts |
Speaker note / any HTML comment <!-- --> |
0 lines — comments never render, so they never count |
A slide is a budget of 15 total, not 15 per element. ## title (1) + table 6 rows (6) + 2 bullets (2) + closing paragraph (2 lines) = 11 → fine. Add a 5-line code block and you're at 16 → split.
Speaker notes and other HTML comments do not count — they never render. So an overflowing slide can often be fixed by moving detail into a note rather than cutting it.
Corollary limits that follow from the 15-line cap:
- Title + 1 image → ≤ 8 body lines. Images eat vertical space even though they don't count as lines.
- Text only → up to 15 lines, but aim for 10–12; long lines are hard to read from the back row.
- Code blocks → ≤ 10 lines per slide. More than that, split at logical boundaries (function def, between classes) or move to an appendix slide.
- Tables → ≤ 8 rows incl. header, ≤ 4 columns. Over 4 columns, the table overflows horizontally instead of vertically.
- Bulleted lists → ≤ 10 items. Past that, split around a logical midpoint or convert to a table.
- Never split a bullet or a table row across slides — a bullet cut in half, or a table with a dangling header, reads as broken.
Counting rule of thumb: CJK ≈ 1 line per 38 characters; Latin ≈ 1 line per 75 characters. A 600-character Chinese paragraph is already ~16 lines on its own.
For splitting heuristics, read references/slide-density.md.
Image asset prep
When converting external image URLs (HTML course content, scraped diagrams):
- Download all unique images to a local
assests/(orassets/) directory. - Compress each to ≤ 100 KB using Pillow + sips. Convert complex diagrams with many colors to JPEG q=82; keep simple line art as PNG with
optimize=True. - Rename with a chapter prefix so paths are stable across slides:
L1_1.1-01_section-overview.pnginstead of raw URL slugs. - Build a URL → local-name mapping before generating slides so replacements are deterministic.
For full prep workflow, read references/asset-prep.md.
CSS bootstrap
All UPerform / openclaw marp decks share one palette (red + deep navy-black theme). The complete style block lives in references/style-bootstrap.md. The palette is calibrated against openclaw_lesson01.md and the fde_lesson*.marp.md family.
It includes:
- CJK font stack (PingFang SC / Microsoft YaHei / Noto Sans CJK SC)
- Cover gradient (deep navy-black
#1a1a2e→ red#c0392b, white text) - Divider slides (deep blue-grey
#2c3e50background + red#e74c3cheading) - Tables (red
#c0392bheader +#f5f5f5alternating rows) — plus thedisplay: table !importantfix, without which marp's default theme makes every table half-width - H1/H2/H3 color scale (red / deep blue-grey / bright red)
- Code blocks (dark
#1a1a2ebackground + green#2ecc71text) - SVG helpers:
svgsizing, CJK font forsvg text, the.t / .tb / .sm / .qt / .lbltype scale, andsection.cover/divider/diagram footer { display: none } - Page-level column layouts:
.cols-2/.cols-3/.cols-main/.split-hgrids, plus in-columnh3/p/ul/tablescaling
Copy the style: |- block verbatim into a new deck's frontmatter.
Speaker notes (HTML comments — invisible in the PDF)
Different audiences need different things. Anything only the speaker needs — background, transitions, backup numbers, Q&A prep — goes in an HTML comment <!-- ... -->:
## Three deployment modes
- Public cloud: fastest to launch
- On-premise: compliance first
- Hybrid: best cost
<!--
Speaker notes:
- "Hybrid" is the newest option; long-time customers ask about it most.
- Case: a bank went on-premise, 8M contract.
- If asked about pricing, jump to the table on the next slide.
-->
Rules:
- Notes are never rendered: absent from the generated HTML, PDF, and PPTX. They exist only in the
.mdsource, which is where the speaker reads them. - Notes do not count toward the 15-line cap — they cost zero lines and can be as long as needed.
- One note block per slide, placed after that slide's content and before the
---separator. - Never write
---inside a note — it is still parsed as a page break and will split the slide. - Also useful for draft markers that must not ship:
<!-- TODO: add real pricing data -->. - Marpit directives (
<!-- _class: ... -->,<!-- _paginate: -->) also use<!-- -->but start with_and stay active. Speaker notes must not start with_. - First fix for an over-15-line slide: ask "is this for the audience or for me?" If it's for you, move the whole thing into a comment — it costs no lines. Only content the audience genuinely needs to read is worth a new slide.
Workflow
- Read source markdown; identify cover, dividers (H1/H2), and content sections (H3+).
- Prep image assets (see asset-prep above).
- Generate slides with these defaults:
- One
## section→ divider slide. - One
### topic→ content slide titled## Topic. - Split so no slide exceeds 15 rendered lines (see the count table above). When a section is long, decide per-paragraph: audience-facing content splits onto a new slide; speaker-only context moves into a
<!-- -->note. - Never split mid-sentence, mid-bullet, or mid-table-row. Cut at paragraph boundaries.
- Image placement per the cheat sheet above.
- For column layouts, use the
cols-*/split-hclasses in references/layout-patterns.md — remember the 15-line cap is per slide, so a 3-column slide gets ~5 lines per column. - For relationship diagrams, route by relationship type → pick the skeleton in references/infographics-svg/skeletons/INDEX.md → optionally swap the shell via metaphors.md → mark the slide
<!-- _class: diagram -->.
- One
- Count lines on every generated slide before rendering. Any slide > 15 → fix now.
- Lint and render:
node scripts/svg-lint.mjs deck.marp.md, thenmarp deck.marp.md --pdf --allow-local-files(add--htmlif the deck contains inline SVG) and visually check first 5 pages plus a sample from middle/end. For a deck with diagrams, render every page to PNG (--images png) — SVG geometry is not verifiable from the source. - Iterate on overflow / awkward layout.
Quick troubleshooting
| Symptom | Likely cause |
|---|---|
| Image shows as markdown text instead of background | Path has unescaped spaces — wrap in <...> |
| Two images stack weirdly | Missing vertical keyword for portrait images |
| Content cut off at bottom | Slide has > 15 rendered lines — split, or move speaker-only detail into a <!-- --> note |
| Table rows truncated horizontally | Table too wide — keep ≤ 4 columns or shrink font in CSS |
| Table renders at half width | marp's default theme sets table { display: block } — the block box stretches but the inner anonymous table shrink-wraps. Fix with display: table !important (already in style-bootstrap) |
| Whole slide shows SVG source code | Missing --html at render time (columns do not need it — only inline SVG) |
| Table inside a column is half width | The display: table !important fix was dropped from the style: block |
| All styling gone — default theme, colours lost, columns collapsed | An HTML comment <!-- --> was placed inside the YAML frontmatter, breaking the YAML parse so the whole style: block is silently dropped. Comments go after the closing --- |
| Table inside a column overflows sideways | Column count too high for the narrower column — cut columns or add font-size: 0.66em |
| Column bottoms look ragged | Missing align-items: start on the grid |
| 3-column slide is an unreadable wall of text | The 15-line cap is per slide; a 3-column slide gets ~5 lines per column — split into separate pages |
| SVG fine in Obsidian reading mode but exports as source in PDF | Obsidian's built-in exporter flattens inline HTML/SVG — export with Marp CLI --html --pdf, or use the Enhanced PDF Export plugin. The deck is not at fault; see references/infographics-svg/craft/marp-compat.md §4 |
| Obsidian's Marp plugin exports SVG as source | Its export command omits --html; patch main.js (2 sites in the il() function) — see references/infographics-svg/craft/marp-compat.md §4.1 |
| SVG diagram disappeared entirely | A blank line inside the SVG block split the HTML block — remove it |
| One slide split into two | A --- inside the SVG or inside an HTML comment |
| All diagrams' arrowheads look identical | Duplicate marker id across SVGs in one merged HTML export — suffix per diagram |
| White text invisible on an arrow shape | Drawn as a hollow chevron — use a 5-point path (rectangle + right tip) and centre text on the rectangle |
| Chinese label overflows its node | SVG <text> does not wrap — shorten the label or split it across <tspan> lines |
| Funnel doesn't look like a funnel | Bands were drawn layer-by-layer with drifting slopes, or the rim/spout is missing — use the single-generatrix template (s=0.9, no gaps) in flow-cycle |
| SVG text ignores the deck's CSS classes | A font-size attribute was hardcoded, or the class was declared in an SVG-internal <style> instead of the frontmatter style: block |
| Axis title / legend overlaps the footer | Add <!-- _class: diagram --> to that slide |
| Slide split awkwardly | Cut mid-bullet or mid-table-row — re-split at paragraph boundaries |
| Speaker note text visible in the PDF | It's not inside an HTML comment — wrap it in <!-- ... --> |
| One slide became two / empty slide appeared | A --- inside an HTML comment is still a page break — remove it |
| Cover slide has wrong background | Missing <!-- _class: cover --> directive |
| PDF render fails silently on images | Path doesn't resolve from .md file location — invoke marp from a directory where the relative path is valid |
For deeper marpit syntax reference, consult https://marpit.marp.app/ — particularly the image-syntax and slide-layouts pages.
