markdown-to-epub
Turn Markdown into a clickable + listenable EPUB, optionally with a branded cover.
The engine is pandoc; this skill is the recipe, one stylesheet, and a cover
generator. Keep the conversion itself scriptless (see "Do not build").
Why EPUB
It keeps links clickable (including ...&t=<seconds> video deep-links) and is
text-to-speech readable on Kindle. Print and PDF are neither. A cover image makes
it show as a real book in the Kindle library instead of a blank placeholder.
Step 0 - preflight the core dependency (pandoc)
Pandoc is the ONE required dependency. Before converting, run:
python "<SKILL_DIR>/scripts/setup/pandoc_preflight.py"
Exit 0 = ready. Exit 2 = missing: it prints the OS-specific install command (winget/choco/scoop, brew, apt/dnf/pacman) and https://pandoc.org/installing.html. Relay that and stop; do not auto-install a system package. (The cover step below additionally needs Pillow; the core conversion does not.)
Convert from the Markdown SOURCE, never from PDF or DOCX
Markdown holds the real links and headings. PDF->EPUB is lossy. If only a PDF/DOCX
exists, convert THAT to markdown first (pandoc in.docx -o in.md) as a last resort.
Branded cover (optional; default ON for briefings and shared reading books)
Pandoc cannot compose a titled cover, so scripts/make_cover.py (Pillow) builds a
portrait PNG: brand background + logo + title + subtitle + author + a DATE stamp
(so multiple same-month versions are distinguishable). Defaults are the Magma
identity (navy #0C2749, the bundled assets/magma-square-1024-dark.png, Montserrat,
Signal Red date). Then pass the PNG to pandoc via --epub-cover-image.
python "<SKILL_DIR>/scripts/make_cover.py" \
--title "Marketing Skills Mastery" \
--subtitle "Corey Haines - a practitioner's guide" \
--date 2026-07-21 \
--out "/tmp/cover.png"
# then add to the pandoc call: --epub-cover-image="/tmp/cover.png"
- The DATE is a real ask: it defaults to today (YYYY-MM-DD) and is meant to distinguish versions across a month. Pass the document's own date for briefings.
- Cover is a build intermediate; write it to a temp path, not next to the .epub.
- Needs Pillow:
pip install pillow. If absent, make_cover.py exits 2 with that message; build the EPUB WITHOUT--epub-cover-image(core still works). - REBRAND (this is a template): swap
--logo <your-square-logo.png>(a square PNG that may include your wordmark, as Magma's does) and override--bg,--title-color,--subtitle-color,--accent. Others drop their own logo intoassets/and pass it; nothing else changes.
Single file
pandoc "note.md" \
-f markdown+autolink_bare_uris \
-t epub3 \
--toc --toc-depth=2 \
--metadata author="Daniel Zivkovic" \
--metadata lang="en" \
--epub-cover-image="/tmp/cover.png" \
--css "<SKILL_DIR>/epub.css" \
-o "note.epub"
+autolink_bare_uristurns barehttps://...URLs into clickable<a>anchors (the&becomes&in XHTML - correct, resolves on Kindle).- Title: pandoc reads the YAML front-matter
title:if present (the Kindle library title). Else add--metadata title="...". - Drop
--epub-cover-imageto skip the cover.
Many files -> one book (the aggregation case)
Pandoc concatenates inputs in argument order, then builds ONE combined TOC. No script needed - this native behavior IS the book builder.
pandoc "intro.md" "artifact-1.md" "artifact-2.md" \
-f markdown+autolink_bare_uris \
-t epub3 \
--file-scope \
--toc --toc-depth=2 \
--metadata title="<topic> - a reading book (<date>)" \
--metadata author="Daniel Zivkovic" \
--metadata lang="en" \
--epub-cover-image="/tmp/cover.png" \
--css "<SKILL_DIR>/epub.css" \
-o "<topic>-book.epub"
--file-scopeisolates each file's heading/footnote IDs so unrelated artifacts do not collide. OMIT it only if links BETWEEN the bundled files must resolve.- TOC entries come from HEADINGS, not filenames: each artifact needs a meaningful
# H1. intro.mdis optional (first, for real introductory prose); a title page from--metadata title=/author=is generated by default without it.
Output location (and the Downloads overlay)
- Single file: write the
.epubnext to the source (same folder + basename) as the canonical copy, so it sits with any.md/.docx/.pdfsiblings. THEN also drop a copy in the user's Downloads for easy Send-to-Kindle. - Bundle of many: there is no single source folder, so write the book straight to Downloads.
- Resolve Downloads OS-AGNOSTICALLY - never hardcode a path:
DL="$(python "<SKILL_DIR>/scripts/downloads_dir.py")" # honors Linux XDG; ~/Downloads elsewhere cp "note.epub" "$DL/" - ALWAYS name the exact output path(s) in your reply (both the canonical path and the Downloads copy) so the user never has to hunt for the file.
Verify (do for link-heavy briefings)
unzip -q -o out.epub -d _check
grep -rhoE 'href="https://www.youtube.com/watch\?v=[A-Za-z0-9_-]+&t=[0-9]+"' _check | wc -l # == source link count
grep -oE '<item[^>]*properties="cover-image"[^>]*/>' _check/EPUB/content.opf # cover present
Delivery
The user sends the .epub to Kindle themselves (Send-to-Kindle email/app). Do not
attempt to push it anywhere.
Do not build
Native pandoc covers the conversion. The ONLY two code files are asset/environment
helpers, not conversion logic: scripts/setup/pandoc_preflight.py (detects the
dependency) and scripts/make_cover.py (composes a cover image pandoc cannot make).
Do NOT add: a conversion wrapper, a Lua filter, a file-discovery framework, a config
system, an EPUB post-processor, or a Calibre dependency. Add conversion code only
after native multi-input demonstrably fails a real case. The stylesheet and the cover
are enhancements; links click and TTS reads without either.