pandoc -f html -t epub3 https://example.com/post -o post.epub looks like a complete product. It is not. Pandoc is a typesetter. It will faithfully encode whatever tree you give it, including the cookie banner.
I run E-Ink, a small web-to-EPUB service. This is the pipeline underneath it, and why the interesting work is not the EPUB zip.
1. Fetch what a reader actually saw
A lot of the pages people want on a Kindle are JS-rendered docs, not static article HTML. A single GET of the URL returns a shell. You need a rendered DOM (we use Firecrawl for this), not wget. That already puts you in “this costs something” territory, which is why a free-forever scraper that also produces pretty EPUBs is usually lying about one of those adjectives.
Public pages only. No login cookies, no paywall punching. If the extract is empty, fail loud.
2. Extract the document, not the application
Readability-style extraction is the whole game. Keep title, byline, headings, lists, blockquotes, <pre><code>, figures + captions. Drop nav, footers, related rails, signup modals, comment widgets.
Two failure modes I still see weekly:
- Under-extract. The article is a docs page whose “body” is a tabbed component. You get the first tab and a blank.
- Over-extract. You kept the sidebar TOC as if it were chapter one.
Markdown is a good intermediate. It is diffable, previewable, and Pandoc eats it. If the Markdown looks wrong, do not bother emitting EPUB. We show a Markdown preview in the UI for exactly this reason.
3. Headings → spine, not a wall of <p>
EPUB readers live and die by the nav document. A 8,000-word essay with one <h1> and no <h2> is a brick. A docs page that uses <h3> for everything produces a TOC that is either empty or 90 entries deep.
Normalize heading levels against the document you actually got, not against the HTML5 spec the CMS ignored. Promote or demote so the spine has a usable chapter list. This is boring string work and it is more important than your CSS.
Images: resolve src / srcset / data-src, download, cap dimensions, rewrite to package-relative paths. Lazy-load placeholders are the number one “why is my ebook a gray box” bug.
4. Pandoc for EPUB3, not for fetching
Once you have a clean Markdown document plus a media folder, Pandoc is the right tool:
pandoc article.md \
--from markdown \
--to epub3 \
--epub-title="..." \
--output article.epub
Let Pandoc write the zip, the nav, the OPF. Do not hand-roll EPUB unless you enjoy debugging mimetype-must-be-first-and-uncompressed for the tenth time.
Output formats beyond EPUB are the same document, different writer: PDF, Markdown, or a second pass to MOBI for older Kindles. Newer Kindles take EPUB natively. Do not make MOBI the default in 2026.
The public API is that pipeline with a credit meter
Same steps, HTTP in front. Bearer token, one URL (or several), a format:
curl -X POST https://e-ink.me/api/v1/convert/webpage \
-H "Authorization: Bearer eink_your_api_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://e-ink.me", "format": "epub"}' \
-o article.epub
Webpage → ebook is 3 credits per URL. File conversion, translation, TTS, and mind maps are separate endpoints on the same key. Docs, OpenAPI, llms.txt, and an MCP manifest live at e-ink.me/en/developers. If you want the same calls inside Cursor or Claude Code, there is a skill at github.com/Selenium39/e-ink-skill — npx e-ink-skill and EINK_API_KEY.
CSS and the e-ink constraints
Ship almost no CSS. E-readers ignore half of it and fight the other half. Relative font sizes, default margins, break-inside: avoid on pre/code and figures. No position: fixed. No dark-mode media queries that assume an emissive screen. Code blocks need wrapping or they clip on a 6-inch Kindle; that is a content problem, not a theme problem.
If you are tempted to embed a webfont: don't, unless you have tested the file on a 2018 Kobo. The reader already has a font. Your job is structure.
What we refuse to do
- No session cookies. If the page needs a login, it is not a public article.
- No “print the whole SPA as screenshots.” That is a PDF of a website, not a book.
- No storing the source file as a convenience cache. Uploads live long enough to produce the output, then they are deleted. The in-browser readers do not upload at all.
I did not invent readable extract or Pandoc. I glued them so I could stop sending navbar novels to a Kobo. If you are building your own pipeline, steal the shape: render → extract → structure → package. If you just want the file, paste a URL on the site. New accounts get free credits.
Questions / broken extracts: openminimax@gmail.com. I am Selenium39.
Top comments (0)