Why Valid EPUB 3 Crashed Amazon KDP: Engineering an Automated E-Book Compiler
Software Architecture, DevOps & System Design
In this article
- Why software architects and technical authors compiling complex Markdown manuscripts face unexpected ingestion failures across digital publishing platforms.
- What hidden layout engine limitations and text-shaping bugs lurk inside Amazon Kindle's proprietary KFX compiler despite passing valid EPUB 3 markup.
- When to ditch brittle HTML/CSS equation hacks and raw ASCII diagrams during automated multi-store e-book production.
- Where strict XML parsing, uncompressed OCF container byte boundaries, and crawler font-asset requirements diverge across EPUBCheck, Google Play, and Amazon KDP.
- How I built an end-to-end Node.js compilation pipeline that converts raw technical Markdown into publication-ready EPUB 3 assets with automated vectorization.
The Bottom Line: Passing W3C EPUBCheck with zero errors guarantees schema validity, not layout survival in proprietary e-reader runtimes. When Amazon KDP crashed our 55,000-word distributed systems manuscript with an unhelpful internal error, we discovered that surviving production ingestion requires replacing KaTeX HTML spans with native W3C MathML, converting ASCII box diagrams into standalone SVG vector cards, enforcing strict XHTML semantics, and packaging containers through a deterministic two-stage zip process.
The Illusion of the "Zipped Website"
This was the very first time I was writing a book. In my day-to-day engineering life, I design distributed architectures, tune microservices, and draft complex technical specifications. But authoring an entire 55,000-word book from scratch was completely unchartered territory. Because I was doing this for the first time, every single step along the journey—from structuring twenty dense chapters so concepts built upon each other logically, to wrestling with technical typography, formatting mathematical proofs, and deciphering digital publishing tooling—was brand new to me. I had zero prior publishing experience to lean on, which meant every roadblock felt novel and every technical problem I ran into was something I was seeing for the very first time.
Yet, after months of deep writing, when I finally typed the last sentence of the manuscript, I innocently assumed that the hardest work was behind me and that the publishing step would be a smooth victory lap.
Like many software engineers, I viewed the EPUB standard through a developer's mental model: an EPUB file is just a zipped website. You take your structured Markdown source files, run them through a parser to generate HTML and CSS, package them with a package manifest and a navigation document, compress the folder into a .zip archive, rename the extension to .epub, and hand it off to digital distributors.
After wiring up an automated build script in Node.js, I ran the resulting archive through the official W3C EPUBCheck 5.3 validation tool.
The terminal output was pristine:
Validating using EPUB version 3.3 rules.
No errors or warnings detected.
EPUB validation completed successfully.Zero warnings. Zero errors. Pure green.
I uploaded the file to Google Play Books and Apple Books, and both accepted the package. Then I uploaded the exact same .epub file to Amazon Kindle Direct Publishing (KDP).
Three minutes later, Amazon's ingestion dashboard greeted me with a stark red banner:
Kindle conversion has encountered an internal error. Our team is actively working to resolve this issue...
There was no error log, no file path, no line number, and no stack trace. Just an opaque failure that brought our deployment pipeline to a grinding halt. Much like navigating Sev-1 outages at scale, when you encounter an unhandled critical failure with zero runtime observability, your first task is blast radius containment and establishing systematic diagnostics into what downstream dependencies are rejecting.
That moment shattered my foundational assumption. Passing W3C EPUBCheck with 0 errors does not mean your book will publish.
The digital publishing ecosystem does not run on a unified browser engine. It is an unstandardized landscape of divergent ingestion pipelines, legacy format converters, and aggressive proprietary text-shaping engines.
| Ingestion Engine | Target Standard | Parsing / Layout Engine | Primary Failure Mode | Diagnostic Feedback |
|---|---|---|---|---|
| W3C EPUBCheck 5.3 | EPUB 3.3 / XML Schemas | Jing / Saxon XML validator | Malformed XML syntax, missing manifest IDs, schema violations | Precise file and line numbers |
| Google Play Books | EPUB 3.0+ | Custom Android WebKit & Cloud Ingestion Crawler | Hanging background asset queries, unresolved external fonts | Generic step timeout ("Step 1 of 6") |
| Amazon KDP | KFX (Kindle Format 10) / KF8 | Proprietary C++/Java Layout & Text-Shaping Pipeline | Null pointers on modern CSS, math span overflows, astral emojis | Opaque "Internal Error" |
To turn our raw manuscript into a production-grade publication that scaled across every platform without manual intervention, I had to stop treating EPUB as a static website. I needed to build a dedicated, multi-stage compiler.
Phase 1: W3C EPUB 3.3 Compliance and the Strict XML Trap
The first phase of the pipeline required building a package that satisfied the EPUB 3.3 specification. While modern web browsers are notoriously forgiving—happily rendering unclosed tags, quirky attributes, and broken entities—the EPUB Open Container Format (OCF) operates under unforgiving XML parsing rules.
Why must the mimetype file sit at byte 0 uncompressed in an EPUB container?
An EPUB file is technically a ZIP archive, but it has a unique structural requirement: operating systems, desktop indexers, and distributor ingestion daemons must be able to identify the file's MIME type without parsing the ZIP central directory table.
Under the EPUB OCF specification, the very first file inside the container must be named mimetype. It must contain the raw ASCII string application/epub+zip, and it must sit at byte 0 of the archive with zero compression (stored mode).
When an e-reader or validator inspects an EPUB file, it reads the initial 68 bytes of the binary header. If byte 30 does not begin the exact ASCII string application/epub+zip, or if the compression flag indicates DEFLATE compression, the file is rejected immediately as corrupt.
Standard archiving utilities like the macOS graphical Archive Utility, typical GUI zip programs, or naive Node.js archive scripts compress files alphabetically or sort by last-modified timestamp. If your script compresses META-INF/ first, the mimetype file is pushed deep into the binary stream.
To solve this deterministically in our automated build script, I decoupled archive creation into a strict two-stage packaging process:
# Stage 1: Add mimetype at byte 0 with ZERO compression (-0) and no extra attributes (-X)
zip -0Xq book.epub mimetype
# Stage 2: Append the rest of the book with maximum compression (-9), recursive (-r)
zip -9Xqr book.epub META-INF OEBPSBy executing this two-stage shell sequence in Node.js, we guarantee that mimetype is always placed first in the archive stream, uncompressed and bit-aligned to byte offset zero.
From Markdown AST to Strict XHTML
Standard Markdown compilers (such as basic Marked or markdown-it) output loose HTML5. For EPUB 3, every document in the OEBPS/ directory must be valid XHTML (application/xhtml+xml). The moment an e-reader's XML parser encounters a standard HTML5 quirk, it throws a fatal XML well-formedness error.
Our compiler pipeline intercepts the Markdown Abstract Syntax Tree (AST) to enforce four strict XHTML transformations:
- Self-Closing Void Tags: HTML5 allows void tags like
<hr>,<br>, and<img>to remain unclosed. In XHTML, an unclosed<hr>halts parsing immediately. Our generator overrides the Markdown token renderers to guarantee<hr />,<br />, and<img src="..." alt="..." />. - Obsolete Attribute Normalization: Older Markdown extensions often render attributes like
align="left"oralign="center"on table cells. In modern XHTML, these attributes are invalid schema violations. We parse table cell tokens and rewrite them to inline styles:style="text-align: left;". - HTML5 Named Entities to UTF-8: Web developers routinely write entities like
—,…,™, and . While HTML5 parsers recognize over 2,000 named character entities, strict XML only recognizes five predefined entities:",&,',<, and>. Any other named entity causes an instant XML validation crash unless declared in an external DTD. Our compiler converts all named entities directly into their native UTF-8 Unicode characters (—,…,™) or numeric hexadecimal references ( ). - GFM Tasklists to Unicode Glyphs: GitHub-Flavored Markdown task lists (
- [ ]and- [x]) typically compile into interactive input checkboxes:<input type="checkbox" disabled />. Because EPUB reflowable profiles prohibit interactive form controls in standard text flows, our AST preprocessor converts these into accessible Unicode ballot box glyphs:☐(\u2610) for unchecked items and☑(\u2611) for completed items.
Phase 2: Asset Integrity and Google Play's URL Crawler
Once the manuscript passed EPUBCheck, I submitted the artifact to Google Play Books. The upload succeeded, but processing stalled indefinitely. The book lingered on "Processing: Step 1 of 6" for forty-eight hours before failing with an uninformative processing error.
The problem traced back to our CSS assets and how distributor backend systems validate asset integrity.
What causes silent processing hangs in Google Play Books when an EPUB passes validation?
When our pipeline converted LaTeX equations into mathematical expressions, we initially bundled KaTeX's companion stylesheet (katex.min.css). That stylesheet included standard @font-face definitions declaring WOFF2, WOFF, and TTF font variants:
@font-face {
font-family: 'KaTeX_Main';
src: url('fonts/KaTeX_Main-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}To minimize final file size, our compiler had not embedded the heavy font binaries into the EPUB container. Because EPUBCheck only validates local file paths listed inside the OPF manifest, it didn't flag the missing physical font files.
Google Play Books, however, deploys an automated asset crawler during ingestion. When the crawler parsed our inlined stylesheets, it attempted to resolve every @font-face URL. Because the files did not exist inside the package and the ingestion sandbox blocked outbound public internet access, the asset resolver repeatedly hung waiting for socket timeouts, eventually failing the entire book.
The fix was straightforward: we added an automated CSS sanitization step to the compilation pipeline. The build script stripped all @font-face blocks from the embedded styles using a regex pass, forcing e-readers to render equations using system-native fonts until we re-engineered the math rendering engine entirely.
Phase 3: The Amazon KDP "Internal Error" Mystery
Resolving the Google Play crawler issue brought us to the real hurdle: Amazon Kindle Direct Publishing.
Our EPUB 3 archive was mathematically valid according to EPUBCheck 5.3. Google Play Books, Apple Books, and desktop readers like Calibre and Thorium rendered the file without incident. Yet, uploading to Amazon KDP consistently resulted in an immediate layout engine crash:
Kindle conversion has encountered an internal error.
Our team is actively working to resolve this issue...Why does an EPUB 3 file that passes EPUBCheck with zero errors crash Amazon KDP?
EPUBCheck is an XML schema and grammar validator. It verifies that your tags are closed, your manifest entries match your directory structure, and your XML namespaces are correct.
Amazon KDP, however, does not display raw EPUB archives. When you upload an EPUB to Amazon, their backend transpiles the file into Kindle Format 10 (KFX), an internal, compiled format optimized for Amazon's e-ink and mobile rendering engines.
The KFX conversion pipeline consists of a complex C++ and Java layout compiler. It executes aggressive hyphenation dictionaries, text-shaping algorithms, dynamic line-breaking calculations, drop-cap sizing, and pre-computed page-turn boundaries.
If the KFX compiler encounters a DOM tree, CSS rule, or Unicode sequence that violates its internal assumptions, it doesn't log a warning—it throws an uncaught exception, aborts the conversion job, and sends an opaque server error back to the publisher portal.
Much like conducting a post-mortem to quantify incident impact, tracking down silent failures in proprietary systems demands methodical, hypothesis-driven forensic isolation. Through days of binary search debugging—stripping down chapters, sections, and individual paragraphs—I isolated five distinct architectural failure modes that reliably crashed Amazon's ingestion engine.
1. The height: inherit Parser Defect
Our early build used KaTeX to render inline math symbols and radical signs. To preserve aspect ratios, KaTeX injected an inline SVG rule:
<svg style="width: 0.4em; height: inherit;" viewBox="...">Web browsers resolve inherit up the DOM tree without issue. The Kindle KFX text-shaping engine, however, operates with a simplified CSS parser. When its layout engine encountered height: inherit on an embedded vector symbol, it attempted to resolve a computed pixel height from an inline parent container that lacked an explicit height definition. The parser hit a null-pointer dereference and crashed the compilation process.
Changing our preprocessor to replace height: inherit with explicit values (height: 1em or height: auto) immediately bypassed the failure.
2. Browser DOM Math Hacks vs. Native MathML
The most significant architectural challenge in compiling our manuscript was rendering technical mathematics. Our book contained over 100 equations covering consensus quorum sizes, network latency percentiles, and throughput calculations.
KaTeX's default output mode generates complex, deeply nested HTML <span> trees. To align numerators, denominators, radicals, and superscripts, it relies heavily on absolute positioning and negative vertical margins:
<!-- Fragile browser DOM simulation of a fraction -->
<span class="katex">
<span class="katex-html">
<span class="base">
<span class="mord">
<span class="mopen nulldelimiter"></span>
<span class="mfrac">
<span class="vlist-t vlist-t2">
<span class="vlist-r">
<span class="vlist" style="height: 0.845em;">
<span style="top: -3.113em;">
<span class="pstrut" style="height: 3em;"></span>
<span class="mord"><span class="mvar">N</span></span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>While this nested structure renders cleanly in Blink, WebKit, or Gecko, it overwhelmed Amazon's KFX layout engine. The KFX engine calculates reflowable page breaks based on predicted line boxes. When faced with negative margins (margin-top: -0.5em) and calculated offsets (top: -3.113em) scattered across hundreds of formulas, its line-breaker stack overflowed.
How should mathematical equations be compiled for Kindle EPUBs to avoid KFX crashes?
Publishers historically worked around this problem by rasterizing every equation into a PNG image. However, rasterizing math creates an awful reading experience: images don't scale when readers adjust font sizes, dark-mode styling breaks into harsh white rectangles, and image baselines misalign with surrounding text.
The proper architectural solution is compiling LaTeX expressions directly into W3C Presentation MathML using KaTeX's output: 'mathml' configuration.
import katex from 'katex';
function compileMathToMathML(latexExpression, displayMode = false) {
return katex.renderToString(latexExpression, {
output: 'mathml',
displayMode: displayMode,
throwOnError: false
});
}This transforms complex LaTeX equations into clean, semantic XML:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<mrow>
<mi>Q</mi>
<mo>=</mo>
<mrow>
<mo>⌊</mo>
<mfrac>
<mi>N</mi>
<mn>2</mn>
</mfrac>
<mo>⌋</mo>
</mrow>
<mo>+</mo>
<mn>1</mn>
</mrow>
</math>Modern Kindle devices and KFX compilers natively support Presentation MathML.
| Approach | Rendering Mechanism | KFX Ingestion Stability | Payload Overhead | Responsive Scaling / Dark Mode | Accessibility |
|---|---|---|---|---|---|
| KaTeX HTML / CSS | Deeply nested <span> elements, negative margins, absolute positioning | High Crash Risk (Line-breaker calculation overflow) | Heavy (~126 KB per chapter with inline CSS) | Poor (Breaks on font resize) | Poor (Screen readers struggle with layout spans) |
| Rasterized PNGs | Pre-rendered static bitmap images | Stable | Massive (~5 MB+ total asset bloat) | Terrible (Blurs on zoom, white boxes in dark mode) | Zero (Requires manual alt-text tagging) |
| Native W3C MathML | Semantic XML elements (<math>, <mfrac>, <msup>) | Flawless (Native KFX layout engine support) | Ultra-light (~43 KB per chapter, zero font bloat) | Perfect (Scales with font slider, adapts to theme) | Native (Recognized by assistive technology) |
Switching our build pipeline to native MathML resolved the KDP conversion crash, reduced individual chapter file sizes from 126KB to 43KB, and eliminated all font dependencies.
3. The 4-Byte Astral-Plane Emoji Trap
Like many technical authors, I used callout boxes throughout the text to emphasize important architectural concepts, prefixing them with Unicode symbols like 💡 (Tip), ⚠️ (Warning), and 📌 (Note).
In UTF-8, these modern emojis reside in the astral plane (Unicode code points above U+FFFF), encoded as 4-byte sequences (or surrogate pairs in UTF-16).
While modern mobile devices handle emojis natively, Amazon's e-ink firmware and KFX hyphenation dictionaries hit an edge case when analyzing surrogate-pair boundaries inside formatted callout blocks. In several chapters, the KFX layout engine threw an invalid character width exception during paragraph pagination.
Our compiler eliminated this vulnerability by replacing raw emojis with semantic text badges:
<!-- Before: Markdown source with raw emoji -->
> 💡 **Architectural Trade-off:** High availability requires loose consistency coupling.
<!-- After: Compiler AST transformation to semantic XHTML -->
<blockquote class="callout callout-tip">
<p><span class="badge badge-tip">TIP</span> <strong>Architectural Trade-off:</strong> High availability requires loose consistency coupling.</p>
</blockquote>Combined with clean, monochrome CSS borders (border-left: 3px solid #666), this approach removed the layout crash risk and ensured consistent rendering across older e-ink screens.
4. Interactive Attributes and Modern CSS Prohibitions
Our technical manuscript featured extensive syntax-highlighted code snippets. We utilized Shiki, a modern syntax highlighter based on TextMate grammars.
By default, modern code highlighters output interactive web attributes:
<pre class="shiki" tabindex="0"><code>...</code></pre>The tabindex="0" attribute allows keyboard users to scroll wide code blocks on web browsers. But in an EPUB reflowable document, Amazon's KFX compiler treats tabindex on non-form elements as an illegal interactive attribute, halting ingestion.
Furthermore, we had to sanitize our global stylesheets against modern web design patterns:
- No Flexbox or CSS Grid: Layout constructs like
display: flexordisplay: gridcause undefined behavior in Kindle's multi-column pagination engine. We reverted all structural layouts to predictable block-level primitives (display: block,display: inline-block). - No
max-widthon the<body>tag: Constraining body width (e.g.,body { max-width: 800px; margin: 0 auto; }) breaks Kindle's ability to divide content into balanced multi-column views on landscape tablets and desktop apps.
5. Cover Image Manifest Wiring & KF8 Backward Compatibility
The final ingestion check on Amazon KDP validates catalog assets. If a book passes compilation but lacks specific legacy metadata, the pipeline fails silently or flags the book with missing cover artwork warnings.
Even in an EPUB 3 file, Amazon's conversion service looks for legacy EPUB 2 OPF manifest bindings to generate store thumbnails for older Kindle Paperwhite and Oasis devices:
<!-- In package.opf metadata -->
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title>Distributed Systems Architecture</dc:title>
<!-- EPUB 2 legacy cover reference required by KDP -->
<meta name="cover" content="cover-image" />
</metadata>
<manifest>
<!-- EPUB 3 cover-image property -->
<item id="cover-image" href="images/cover.jpg" media-type="image/jpeg" properties="cover-image" />
<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml" />
</manifest>
<spine>
<itemref idref="cover" linear="no" />
</spine>
<!-- EPUB 2 guide block for Kindle KF8 legacy compatibility -->
<guide>
<reference type="cover" title="Cover" href="cover.xhtml" />
</guide>Providing both the EPUB 3 properties="cover-image" attribute and the legacy <meta name="cover"> tag satisfies both modern KFX processors and older KF8 conversion workers.
Phase 4: Reflowable Text vs. Fixed Architecture Diagrams
With the book successfully compiling through KDP, I encountered a major visual issue in the previewer: our architecture diagrams were unreadable.
Why do ASCII architecture diagrams break on reflowable e-readers, and how can an automated compiler fix them?
As engineers writing in Markdown, our default instinct for architecture flowcharts is ASCII art:
+-------------------+ RPC +-------------------+
| Ingestion Node | -------------> | Consensus Leader |
+-------------------+ +-------------------+
| |
| Write-Ahead Log | Commit
v v
+-------------------+ +-------------------+
| Local NVMe | | State Machine Rep |
+-------------------+ +-------------------+On a desktop monitor inside VS Code, an ASCII diagram is clean, expressive, and easily version-controlled. But on a reflowable e-reader, that fixed 2D grid collides with variable screen viewports.
A Kindle Paperwhite or smartphone screen in portrait orientation typically has an effective viewport width of 320px to 360px. At standard monospace font sizes, that allows for roughly 35 to 42 characters per line.
When an ASCII diagram built for 70 columns is rendered on an e-reader, two layout failures happen:
- The
pre-wrapShredder: If your CSS applieswhite-space: pre-wrap, the layout engine wraps lines at character 40. Horizontal arrows snap in half, right-side border lines wrap beneath content, and the diagram disintegrates into visual noise. - The
white-space: preTrap: If your CSS enforceswhite-space: pre, the e-reader refuses to wrap lines. But e-ink readers do not support horizontal scrollbars; a horizontal swipe gesture triggers a page turn. Any diagram wider than 40 characters is permanently clipped at the right margin. - The Font Scaling Slider: Even if a diagram fits on default settings, the moment a reader increases font size from level 2 to level 5, the monospace line length expands and clips beyond the viewport boundary.
The Automated Vector Diagram Pipeline
I didn't want to redraw 40 architecture diagrams manually in Figma or Illustrator. Doing so would sever the connection between the manuscript source text and our version control system.
Instead, I wrote an automated ASCII-to-SVG vector compiler directly into our Markdown parsing pipeline.
Whenever the parser encounters a code block, it executes a regex heuristic to detect box-drawing characters and directional arrows:
function isAsciiDiagram(codeText) {
const boxDrawingPattern = /[┌─┐│└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▲▼►◄←↑→↓│]/;
const connections = /\+--|\+==|-->|<--|\|/;
return boxDrawingPattern.test(codeText) || connections.test(codeText);
}If the block is an ASCII architecture diagram, the compiler:
- Calculates the maximum line width and total line count across the text matrix.
- Generates an SVG container with an exact
viewBox="0 0 width height", computing character dimensions based on a fixed monospace font ratio (e.g., character width = 8.5px, line height = 18px). - Wraps each line in an SVG
<text>and<tspan>element with locked coordinates, applying clean typography, high-contrast borders, and adaptive background colors. - Writes the resulting vector file out to
OEBPS/images/diagram-[hash].svg. - Replaces the Markdown code block with a responsive image element wrapped in an XHTML
<figure>container. - Automatically registers the generated SVG asset in the OPF package manifest with
media-type="image/svg+xml".
This transformation yielded a major ergonomic benefit on Kindle devices: device-native double-tap and pinch-to-zoom.
Because the diagram is compiled into a standalone vector image asset rather than raw text, Kindle's KFX layout engine automatically enables full-screen image inspection. When a reader encounters a complex architectural topology on a small e-ink screen, they simply double-tap the diagram. The Kindle OS expands the diagram full-screen, allowing the reader to pinch, zoom, and pan across the topology with vector clarity, then tap once to return to their reading position.
Diagrams vs. Tables: Choosing the Right Representation
During this pipeline overhaul, we also audited our use of ASCII tables. Authors frequently format multi-column comparison tables inside monospace code blocks:
Feature Strategy A Strategy B
------------------------------------------------
Latency Low (p99 < 5ms) Moderate
Availability High (99.99%) Moderate
Consistency Eventual Strict SerialJust like diagrams, monospace text tables break on mobile screens. But unlike flowcharts, tabular data shouldn't be converted into static SVGs. Tabular comparisons should be reflowable, searchable, and accessible.
We systematically migrated all monospace ASCII tables into standard GitHub-Flavored Markdown tables:
| Feature | Strategy A | Strategy B |
| :--- | :--- | :--- |
| **Latency** | Low (p99 < 5ms) | Moderate |
| **Availability** | High (99.99%) | Moderate |
| **Consistency** | Eventual | Strict Serial |Our XHTML generator compiles these into semantic <table>, <thead>, and <tbody> structures. Modern e-readers format HTML tables natively, wrapping individual cell text gracefully while preserving column relationships across portrait and landscape modes.
Phase 5: Architecture of an Automated EPUB Build Pipeline
To eliminate manual steps and ensure repeatability, I consolidated these solutions into a unified Node.js compilation pipeline: generate_epub.js.
The pipeline executes a deterministic five-stage build process:
Here is how the core architectural stages are wired inside the automated compiler.
1. The AST Preprocessor: MathML and Vector Diagrams
Before feeding Markdown into Marked, our preprocessor scans the AST to transform math delimiters and ASCII blocks into strict, accessible standards:
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import katex from 'katex';
export function preprocessMarkdown(rawContent, imagesOutputDir) {
let content = rawContent;
// Transform block math: $$ ... $$ to native MathML
content = content.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
return katex.renderToString(math.trim(), {
output: 'mathml',
displayMode: true,
throwOnError: false
});
});
// Transform inline math: $ ... $ to native MathML
content = content.replace(/\$([^\$\n]+?)\$/g, (_, math) => {
return katex.renderToString(math.trim(), {
output: 'mathml',
displayMode: false,
throwOnError: false
});
});
// Transform ASCII architecture diagrams to responsive SVGs
content = content.replace(/```(?:ascii|diagram)\n([\s\S]*?)```/g, (_, ascii) => {
const hash = crypto.createHash('md5').update(ascii).digest('hex').slice(0, 8);
const svgFileName = `diagram-${hash}.svg`;
const svgFilePath = path.join(imagesOutputDir, svgFileName);
if (!fs.existsSync(svgFilePath)) {
const svgContent = compileAsciiToSvg(ascii);
fs.writeFileSync(svgFilePath, svgContent, 'utf-8');
}
return `\n\n\n\n`;
});
return content;
}2. Custom Marked Renderer for Strict XHTML
To guarantee that Marked produces XML-compliant syntax that passes EPUBCheck without post-processing hacks, we configure a custom XHTML renderer:
import { marked } from 'marked';
export function createXhtmlRenderer() {
const renderer = new marked.Renderer();
// Enforce self-closing horizontal rules
renderer.hr = () => '<hr />\n';
// Enforce self-closing line breaks
renderer.br = () => '<br />\n';
// Enforce self-closing images with explicit alt attributes
renderer.image = (href, title, text) => {
const titleAttr = title ? ` title="${escapeXml(title)}"` : '';
return `<img src="${href}" alt="${escapeXml(text || '')}"${titleAttr} class="responsive-diagram" />`;
};
// Convert table alignments to inline CSS styles instead of obsolete align attributes
renderer.tablecell = (content, flags) => {
const type = flags.header ? 'th' : 'td';
const style = flags.align ? ` style="text-align: ${flags.align};"` : '';
return `<${type}${style}>${content}</${type}>\n`;
};
return renderer;
}
function escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '<';
case '>': return '>';
case '&': return '&';
case '\'': return ''';
case '"': return '"';
}
});
}3. Dual Navigation Generation (nav.xhtml and toc.ncx)
To maintain full compatibility with modern EPUB 3 readers while satisfying Amazon's legacy KF8 conversion requirements, our pipeline generates two navigation files:
OEBPS/nav.xhtml: Modern EPUB 3 navigation document using HTML5<nav epub:type="toc">.OEBPS/toc.ncx: EPUB 2 Navigation Control file formatted in XML, declared in the OPF spine using thetoc="ncx"attribute.
export function generateNavXhtml(chapters) {
const items = chapters.map(ch =>
`<li><a href="${ch.filename}">${escapeXml(ch.title)}</a></li>`
).join('\n ');
return `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
<head>
<title>Table of Contents</title>
<link rel="stylesheet" type="text/css" href="styles/style.css" />
</head>
<body>
<nav epub:type="toc" id="toc">
<h1>Table of Contents</h1>
<ol>
${items}
</ol>
</nav>
</body>
</html>`;
}4. Deterministic Two-Stage Packaging
The final step bundles the build directory into a valid OCF package. Rather than relying on third-party archiving libraries that might alter file order or header compression, our pipeline invokes native system utilities:
import { execSync } from 'child_process';
export function packageEpub(buildDir, outputEpubPath) {
// Ensure output directory exists and clean old artifact
if (fs.existsSync(outputEpubPath)) {
fs.unlinkSync(outputEpubPath);
}
// Stage 1: Store mimetype at byte 0 with zero compression (-0)
execSync(`zip -0Xq "${outputEpubPath}" mimetype`, { cwd: buildDir });
// Stage 2: Deflate compress META-INF and OEBPS recursively (-9Xqr)
execSync(`zip -9Xqr "${outputEpubPath}" META-INF OEBPS`, { cwd: buildDir });
console.log(`[EPUB Compiler] Successfully assembled: ${outputEpubPath}`);
}Key Takeaways & Architectural Lessons
Engineering an automated Markdown-to-EPUB compiler for our 55,000-word distributed systems manuscript was an exercise in understanding the reality of legacy runtime environments versus theoretical specifications.
If you are compiling technical books, architectural documentation, or whitepapers for distribution across major digital publishing platforms, keep these lessons in mind:
- Validators verify grammar, not runtime layout: EPUBCheck passing with zero errors is table stakes, not a deployment guarantee. Amazon KDP converts EPUBs into proprietary KFX binaries via an aggressive layout engine that crashes on unexpected DOM hierarchies. Always test your generated files directly on the Amazon KDP Previewer and Google Play Books partner consoles.
- MathML is the standard for technical publications: Ditch browser-based KaTeX HTML hacks, negative CSS margins, and blurry raster PNGs. Kindle and modern EPUB 3 reading systems natively support W3C Presentation MathML, delivering crisp, theme-aware, accessible equations with minimal payload overhead.
- Keep reflowable CSS simple: Modern web techniques like Flexbox, CSS Grid, absolute positioning, negative margins, and
height: inheritbreak Kindle's reflowable pagination algorithms. Keep your book styles limited to semantic, predictable block elements and simple inline formatting. - Treat ASCII diagrams as graphic artifacts, not text: Monospace ASCII art collapses on mobile viewports and e-ink displays. Compile box-drawing characters into standalone, responsive SVG vector assets. This prevents text wrapping and enables device-native pinch-and-zoom inspection for your readers.
- Code every constraint into a repeatable compiler: Never hand-edit compiled XHTML files or manually assemble ZIP archives. Build your constraints—from uncompressed
mimetypebyte alignment to Unicode entity mapping and dual-navigation manifests—directly into an automated, version-controlled build script.

Sandeep Kumar
Founder & Software Architect | Author of “The Operational State Control Plane”
Electronics engineer, software architect, and author of The Operational State Control Plane. Specializing in system design, distributed resilience, and building scalable tech solutions with real-world engineering insights.
