Merging multiple EPUBs into a single file for Amazon Kindle

Alex West wrote Not Business Advice , a free series of 13 short books about building profitable personal projects, compiled from the daily blog posts he’s written since 2018. It’s the story of how he made over $1M from his own side projects, and I’m reading it right now. It’s really good: honest about the failure, luck, and burnout, not just the wins. Go read it, it’s free.

The 13 books are one long story split into parts, so I wanted them on my Kindle as a single book: one cover, one table of contents, one thing to open and read straight through.

There’s no button for that. But an EPUB is just a ZIP file with an XML manifest inside, so merging them turned out to be a scripting job, not a design job. Here’s how I did it, including the parts that made epubcheck yell at me.

What’s actually inside an EPUB#

Unzip one and it’s not mysterious:

$ unzip -l book_one.epub
  Length      Name
---------    ----
       20    mimetype
      244    META-INF/container.xml
     2126    content.opf
    10727    toc.ncx
      803    titlepage.xhtml
     4863    stylesheet.css
   119662    cover.jpeg
   127796    GoogleDoc/Book1_split_002.xhtml
    ...

Four files do the real work. mimetype declares it’s an EPUB. META-INF/container.xml points at the package file. content.opf is the manifest: it lists every file in the book, the reading order (the spine), and the metadata (title, author, cover). toc.ncx is the navigation, the clickable table of contents. The rest is content: XHTML for the text, CSS, images.

Once you see that, merging is obvious in theory. Take the content documents from all 13 books, drop them into one ZIP, and write one content.opf and one toc.ncx that stitch them together in order. The devil is in doing it without collisions.

Namespace each book, or everything collides#

Every one of those 13 books had a stylesheet.css, a content.opf, a cover.jpeg, and CSS classes named c1, c2, c3 and so on. Dump them all in one flat folder and book 5’s stylesheet clobbers book 4’s, the class names cross-contaminate, and the whole thing renders wrong.

The fix: give each book its own directory inside the merged EPUB and keep its files local to it.

OEBPS/
  cover.jpeg          # one cover for the whole collection
  content.opf         # one master manifest
  nav.xhtml           # one EPUB3 table of contents
  toc.ncx             # one EPUB2 fallback ToC
  book01/
    ch001.xhtml
    stylesheet.css
  book02/
    ...

Because each book’s XHTML only references its own stylesheet.css sitting next to it, the c1/c2 class collisions stop mattering. No rewriting of class names, no shared CSS to reconcile. I did rewrite the relative CSS links (the originals pointed at ../stylesheet.css from a subfolder) and gave the content files clean sequential names, but that’s it.

Then I built one content.opf whose spine lists every book’s chapters in order, and one navigation document with the 13 books as top-level sections, each with its own chapters nested underneath. That nested table of contents is what makes it read as one book instead of 13.

Making it read as one book, not 13#

A merge that’s technically valid can still feel like 13 books glued together. Three things sell the single-book illusion:

  • One cover. I dropped all 13 individual covers (each literally said “Book 1”, “Book 2”…) and generated a single collection cover with ImageMagick. If I’d kept book one’s cover, my Kindle library thumbnail would’ve read “Book 1”, which defeats the point.
  • One set of metadata. One dc:title, one dc:creator, one identifier. The Kindle library shows one entry.
  • One nested ToC. Each source book carried its own boilerplate title page (“Not Business Advice”, repeated 13 times). I threw those out and replaced them with a clean “Book N” section divider, then nested each book’s real chapters under it.

I pulled the chapter list for each part straight out of that book’s original toc.ncx and remapped the links to the new file paths. 13 parts, 501 chapter entries, one continuous read.

The gotchas that trip epubcheck#

epubcheck is the official validator, and it’s strict. It’s also exactly what you want, because “valid EPUB” is the closest thing to a guarantee that Amazon’s pipeline will accept the file. A few rules I had to respect:

mimetype must be the first entry in the ZIP, stored uncompressed. This is a hard requirement in the spec, and Python’s zipfile won’t do it for you unless you’re explicit:

import zipfile

with zipfile.ZipFile(out, "w") as zf:
    # first entry, no compression
    zf.write(f"{stage}/mimetype", "mimetype", compress_type=zipfile.ZIP_STORED)
    for path, arc in remaining_files:
        zf.write(path, arc, compress_type=zipfile.ZIP_DEFLATED)

Every file in the ZIP must be listed in the manifest. A stray file that’s in the archive but not in content.opf is an error, not a warning. So I only copied in the files I referenced (the body XHTML and CSS), and left the per-book covers and title pages behind.

Your identifier has to be a real UUID. My first pass used a readable string, and epubcheck wasn’t fooled:

WARNING(OPF-085): "dc:identifier" value
"urn:uuid:...-not-business-advice" is marked as a UUID,
but is an invalid UUID.

Swap in an actual UUID and it’s happy. The final run:

$ epubcheck not-business-advice.epub
Validating using EPUB version 3.3 rules.
No errors or warnings detected.
Messages: 0 fatals / 0 errors / 0 warnings / 0 infos

Zero errors, zero warnings. That’s the result I wanted before going anywhere near a Kindle. (Tested on EPUBCheck v5.3.0, Python 3.14.)

Getting it onto the Kindle#

Modern Kindles accept EPUB directly through Send to Kindle , which converts it server-side. So a valid EPUB is enough. Drag it onto the web uploader or email it to your @kindle.com address and you’re done.

I also made an AZW3, Amazon’s own format, thinking it was the safe Kindle-native option. Calibre’s CLI does it in one line:

$ ebook-convert not-business-advice.epub not-business-advice.azw3
...
Output saved to  not-business-advice.azw3

That turned out to be wasted effort. Send to Kindle flat-out rejects AZW3: its web uploader and email gateway only take EPUB, PDF, Word, and a handful of others. And send an EPUB and Amazon converts it to AZW3 on their own servers anyway, so I’d been generating the exact file they produce for free. AZW3 only earns its keep if you copy it to the device directly over USB, and modern Kindles take EPUB that way too. The one file I needed was the 517 KB EPUB I’d already validated.

I validated this with epubcheck and a clean Calibre conversion, not on physical Kindle hardware. Both are strong signals, but I can’t claim I watched it render on a Paperwhite. If you’ve been down the custom-fonts-on-Kindle rabbit hole like I have, you already know the device firmware has opinions of its own.

If you’re merging a series like this, the whole thing is a few hundred lines of Python (most of it boilerplate HTML for the cover, title page, and dividers) plus two well-known tools. No GUI, no paid app.

I should be upfront about how it got built, though: I hand-wrote almost none of it. I pointed Claude (Opus 4.8) at the download link and described what I wanted, and it drove about 99% of the way there: downloading the books, reverse-engineering the EPUB layout, writing the merge script, running epubcheck, and fixing its own validation errors when they came up. I steered and sanity-checked the output; it did the typing. A well-scoped, fiddly job you can describe precisely is exactly where these tools shine right now .