Imported from borghei/claude-skills (
markdown-html/md-slides/SKILL.md). Install upstream withnpx skills add borghei/claude-skills --skill md-slides. Copyright stays with the author (MIT + Commons Clause).
Markdown Slide Decks
Turn a markdown file into a slide deck that is one HTML file: six layouts, speaker notes, keyboard and remote navigation, light/dark theming, and handout printing. The density linter is the part that matters most — it catches the slides an audience cannot absorb before you are standing in front of them.
When to use this skill
- Building a deck from markdown you want to keep in version control
- Presenting from a laptop without a presentation app or a cloud account
- Cutting an overloaded deck where every slide is a wall of text
- Timing a talk against a fixed slot before rehearsing it
- Converting a document into a deck as a starting point, then editing down
- Producing a handout that includes speaker notes alongside each slide
Inputs the skill expects
- A markdown deck source, slides separated by
--- - The talk length and format — presented live, or circulated to be read
- Layout intent per slide: title, section divider, bullets, two-column, quote, image
- Speaker notes after a
???marker on each content slide - Images as relative paths, or as data URIs for a genuinely single-file deck
- The presentation environment: room lighting and display size
Clarify First
Before building, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- Presented live or circulated to read — why it changes the output: it selects the density profile, and the two budgets differ by roughly 2x; the wrong one produces a deck that fails at the job it actually has
- Talk length and slot — why it changes the output: it sets the slide count and drives the runsheet; a 60-slide deck for a 15-minute slot is an unfinished edit, not a pacing choice
- Whether images must be embedded — why it changes the output: relative paths mean the deck is a folder, not a file, and it breaks when emailed
- Room lighting, if presenting — why it changes the output: dark themes wash out under ambient light; this decides the default theme and the contrast floor
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Workflows
Workflow 1 — Build a deck and check its density
- Lint first. Building an overloaded deck and reading it on screen is a slower way to learn the same thing.
- Fix what the linter flags — usually by moving sentences into speaker notes.
- Build. The theme and navigation script are inlined automatically.
python3 markdown-html/md-slides/scripts/slide_density_linter.py \
--input markdown-html/md-slides/assets/sample_deck.md --profile present
python3 markdown-html/md-slides/scripts/md_to_slides.py \
--input markdown-html/md-slides/assets/sample_deck.md \
--out build/deck.html --format text
Workflow 2 — Time a talk against its slot
- Generate the runsheet at your actual speaking rate, not the default.
- Read the
*markers — those slides have no notes, so their duration is guessed from on-slide content and is the least reliable number in the sheet. - If the total is over, cut slides. Speaking faster does not create time.
python3 markdown-html/md-slides/scripts/notes_runsheet.py \
--input markdown-html/md-slides/assets/sample_deck.md \
--wpm 130 --target-minutes 15 --format text
python3 markdown-html/md-slides/scripts/notes_runsheet.py \
--input markdown-html/md-slides/assets/sample_deck.md \
--format markdown > build/runsheet.md
Workflow 3 — Convert a document into a deck
- Split on every
##heading to get a first pass with the document's own structure. - Lint immediately. The result will fail — a document section carries far more than a slide's budget. That failure list is the edit plan.
- Rewrite headings as claims, demote sentences to notes, then rebuild.
python3 markdown-html/md-slides/scripts/md_to_slides.py \
--input markdown-html/md-slides/assets/sample_deck.md --split-on h2 --out build/draft.html
python3 markdown-html/md-slides/scripts/slide_density_linter.py \
--input markdown-html/md-slides/assets/sample_deck.md --profile present --format json
Decision frameworks
Density budget
| Metric | present target |
Warn | Error | read warn / error |
|---|---|---|---|---|
| Words per slide | <= 40 | 50 | 75 | 90 / 130 |
| Bullets per slide | <= 5 | 6 | 8 | 8 / 12 |
| Words per bullet | <= 8 | 12 | 20 | 18 / 28 |
| Heading characters | <= 50 | 60 | 90 | 70 / 100 |
| Table rows | <= 5 | 6 | 9 | 9 / 14 |
| Code lines | <= 10 | 12 | 20 | 18 / 30 |
Every threshold is a proxy for one rule: a slide must be readable in under 5 seconds, or it competes with the presenter. An audience cannot read and listen simultaneously — when a slide carries prose, the room reads it faster than you can say it and then disengages.
title, section, quote, and image layouts are exempt from the body rules.
Layout selection
| Layout | Use for | Limit |
|---|---|---|
title |
Opening slide | One per deck; heading plus one subtitle line |
section |
Divider between movements | One every 5-8 content slides |
default |
Heading plus content | The workhorse; full density budget applies |
two-column |
A comparison, or image beside explanation | [RECOMMENDED] Not a way to fit twice the content |
quote |
One sentence worth sitting with | One per deck; a second dilutes the first |
image |
Full-bleed visual | Alt text mandatory — the linter errors without it |
Deck length by slot
| Talk length | Content slides | Note |
|---|---|---|
| 5 min | 5-7 | ~45s per slide |
| 15 min | 12-18 | The common conference slot |
| 30 min | 20-30 | Plus 2-3 section dividers |
| 60 min | 30-45 | Needs interaction, not more slides |
The runsheet adds a 4-second transition allowance per slide — real, and routinely forgotten. Thirty slides carry two minutes of dead air before anyone speaks.
Where content goes when a slide is too dense
| Content | Belongs |
|---|---|
| The claim | Slide heading |
| The evidence, compressed | Slide body, at label length |
| The sentences | Speaker notes [PROVEN] |
| The full table | Appendix slide |
| The caveat | Speaker notes, then Q&A |
Contrast at projection
| Context | Minimum |
|---|---|
| Monitor / screen share | 4.5:1 (WCAG AA) |
| Well-lit room | 7:1 |
| Bright room, weak projector | 10:1 |
[PROVEN] Present light in a bright room, dark in a dark one. The T key
toggles theme so this is decided in the room, not an hour before.
Anti-Patterns
The document in slide clothing
Mistake: Full paragraphs on every slide, because the deck must also work as a leave-behind for people who were not there.
Why it happens: It is one artifact instead of two, and the request to "make sure it stands alone" is reasonable on its face.
Instead: Pick one job. A presented deck uses the present budget with the sentences in speaker notes; a circulated deck uses --profile read. Trying to serve both produces something too dense to present and too fragmentary to read. If it will mostly be read, write a document and build a thin deck that points at it.
Bullets as sentences
Mistake: Writing each bullet as a complete sentence, so the slide reads correctly on its own. Why it happens: Fragments feel unfinished while drafting, and complete sentences feel more rigorous. Instead: A bullet is a label the presenter expands, not a sentence the audience reads. Past roughly 12 words it is prose and the room stops listening. Move the sentence into the speaker notes, where it is genuinely useful — that is what notes are for, and it is why the linter flags a dense slide with empty notes.
Topic headings
Mistake: Heading a slide with its subject — "Options", "Results", "Storage costs". Why it happens: It matches how the deck was outlined, and outlines are built from topics. Instead: Write the heading as the sentence you want remembered: "Cold data is paying hot prices", "Latency held; spend fell 31%". Someone who reads only the headings should still receive the argument. This single change improves a deck more than any layout decision.
Speaking faster to fit the slot
Mistake: Discovering the deck runs long and planning to talk quickly rather than cutting slides. Why it happens: Cutting means giving up content you already built and believe in. Instead: Cut. Speaking faster converts an over-long talk into an over-long talk nobody follows, and it eliminates the pauses that let a point land. The runsheet says "cut content, do not speak faster" for this reason.
Skipping the full-screen proof
Mistake: Authoring in a windowed browser and presenting full screen without checking. Why it happens: The deck looks finished on the laptop, and full screen feels like the same thing but bigger. Instead: Open it full screen on the actual display and walk every slide with the actual remote. Type scales with viewport width, so every size decision changes — tables and code blocks are set smaller than body text and are the first things to become unreadable from the back row. Presenter remotes send PageUp/PageDown, which is also worth confirming before you are on stage.
Files
| File | Purpose |
|---|---|
scripts/md_to_slides.py |
CLI: build a self-contained HTML deck with inlined theme and navigation |
scripts/slide_render.py |
Slide splitting, layouts, note extraction, escaping-first renderer — imported by md_to_slides.py, not a CLI |
scripts/slide_density_linter.py |
Flag slides over the word, bullet, table, and code budgets; CI gate |
scripts/notes_runsheet.py |
Timed runsheet from speaker notes; text, JSON, or markdown |
references/slide-density-and-layout.md |
Thresholds and their rationale, layout patterns, deck length |
references/deck-accessibility-and-delivery.md |
Focus management, keyboard interface, projection contrast, pre-flight |
assets/sample_deck.md |
Working deck using every layout; passes the density gate |
assets/deck_theme.css |
Bundled deck theme — this skill's own copy |
assets/deck_nav.js |
Inlined navigation: keyboard, hash routing, notes, theme toggle |
assets/deck_outline_template.md |
Starting structure for a new deck |
All scripts share one exit-code contract: 0 clean, 2 gate failed (findings at or above the threshold), 1 the tool itself errored. A CI job can therefore tell a real defect from a broken invocation.
Three CLI tools, one module. slide_render.py is a library, not a fourth
command — it holds the parser and renderer that md_to_slides.py imports. A
single-file converter came to 324 lines, over the 300-line ceiling, and the only
ways to fit were deleting docstrings or dropping features. Splitting CLI from
parser is the remedy the tool-design standard prescribes for an oversized
script, and same-directory imports keep the package self-contained: nothing here
imports from another skill, and md-document carries its own separate copy of
the equivalent renderer rather than sharing this one.