Inside an EPUB File: Structure, OPF, Navigation, Scripts and Pagination
An EPUB is not a mystery format — it's a ZIP archive containing a specific set of files. Once you understand the structure, you can fix broken EPUBs, build converters, or create files from scratch. This page walks through every part: the archive layout, the OPF package document, the two generations of navigation (NCX and nav document), what JavaScript can and cannot do, and how page breaks and print page numbers work in a format that has no fixed pages.
The EPUB ZIP Structure
mybook.epub (ZIP archive)
├── mimetype ← must be first, uncompressed
├── META-INF/
│ └── container.xml ← points to the OPF file
└── OEBPS/ (or any folder name)
├── content.opf ← package document (manifest + spine)
├── toc.ncx ← EPUB 2 navigation (NCX)
├── nav.xhtml ← EPUB 3 navigation (NAV)
├── chapter01.xhtml ← content files
├── chapter02.xhtml
├── css/
│ └── styles.css
└── images/
├── cover.jpg
└── figure1.png
The mimetype File
The first file in the ZIP must be named mimetype, stored without compression, and contain exactly:
application/epub+zip
No newline, no BOM, no spaces. This is how e-readers and validators identify the file as an EPUB without reading the full archive. Creating EPUBs with Python:
import zipfile
with zipfile.ZipFile('book.epub', 'w') as z:
# mimetype MUST be first and uncompressed
z.writestr(zipfile.ZipInfo('mimetype'), 'application/epub+zip',
compress_type=zipfile.ZIP_STORED)
# All other files can be compressed
z.write('META-INF/container.xml', compress_type=zipfile.ZIP_DEFLATED)
z.write('OEBPS/content.opf', compress_type=zipfile.ZIP_DEFLATED)
META-INF/container.xml
This file tells the reading system where to find the OPF package document:
<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
The full-path is relative to the root of the ZIP. The OEBPS folder name is conventional but not required — you can use any folder name or put the OPF in the root. An EPUB 3 file has exactly one OPF (the multiple-renditions experiment in EPUB 3.0.1 was dropped in EPUB 3.2).
The OPF Package Document (content.opf)
OPF stands for Open Packaging Format. The .opf file (historically content.opf or package.opf) is the heart of an EPUB and the first thing a reading system parses. It answers three questions — what is this book, what files does it contain, and in what order are they read — through four sections:
- <metadata> — Dublin Core metadata (title, author, language, identifier)
- <manifest> — lists every file in the publication with its id, href, and media-type
- <spine> — defines the reading order by referencing manifest item ids
- <guide> — EPUB 2 landmark references (optional, replaced by NAV landmarks in EPUB 3)
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/"
version="3.0" unique-identifier="bookid">
<metadata>
<dc:identifier id="bookid">urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</dc:identifier>
<dc:title>My Book Title</dc:title>
<dc:language>en</dc:language>
<dc:creator>Author Name</dc:creator>
<meta property="dcterms:modified">2026-06-12T00:00:00Z</meta>
</metadata>
<manifest>
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="ch1" href="chapter01.xhtml" media-type="application/xhtml+xml"/>
<item id="ch2" href="chapter02.xhtml" media-type="application/xhtml+xml"/>
<item id="css" href="css/styles.css" media-type="text/css"/>
<item id="cover-img" href="images/cover.jpg" media-type="image/jpeg"
properties="cover-image"/>
</manifest>
<spine toc="ncx">
<itemref idref="nav" linear="no"/>
<itemref idref="ch1"/>
<itemref idref="ch2"/>
</spine>
</package>
Metadata
Required in every EPUB: dc:identifier (must match the unique-identifier attribute on <package>; a UUID or ISBN), dc:title, and dc:language (a BCP 47 code such as en, fr, zh-Hans). EPUB 3 also requires meta property="dcterms:modified" — an ISO 8601 UTC timestamp that must change on every revision. dc:creator, dc:publisher, dc:description, dc:subject and dc:date are optional. Accessibility metadata uses schema.org properties in the same block:
<meta property="schema:accessMode">textual</meta>
<meta property="schema:accessibilityFeature">structuralNavigation</meta>
<meta property="schema:accessibilityHazard">none</meta>
<meta property="schema:accessibilitySummary">This publication meets WCAG 2.2 Level AA.</meta>
Manifest
Every file in the archive except mimetype and the contents of META-INF/ must have an <item> with an id, an href relative to the OPF, and a media-type. A file in the ZIP that is missing from the manifest is an EPUBCheck error, and reading systems may ignore it. The optional properties attribute flags special items: nav, cover-image, scripted, mathml, svg. Common media types: application/xhtml+xml (content and nav document), text/css, image/jpeg, image/png, image/svg+xml, font/otf, font/woff2, application/x-dtbncx+xml (NCX).
Spine
The spine lists the XHTML documents in default reading order by idref. CSS, images, fonts and the nav document do not need spine entries. linear="no" marks items outside the main reading flow (cover page, table of contents) that readers may skip in sequential navigation. The toc="ncx" attribute points at the NCX for EPUB 2 readers.
What EPUB 3 changed
- Nav document replaces NCX — the manifest must include one item with
properties="nav". - dcterms:modified is required and must be updated on every revision.
- Guide element removed — replaced by the landmarks nav.
- Media overlays — a
media-overlayattribute on manifest items links SMIL files for read-aloud sync. - Accessibility metadata — schema.org properties became standard practice.
Content Files — XHTML, Not HTML
Chapter files must be valid XHTML — XML-conformant HTML. Key differences from HTML5:
- Must have the XML declaration or at least the XHTML doctype
- All tags must be closed:
<br/>not<br> - Attribute values must be quoted
- Case-sensitive: use lowercase element names
- The namespace declaration is required:
xmlns="http://www.w3.org/1999/xhtml"
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Chapter 1</title>
<link rel="stylesheet" type="text/css" href="../css/styles.css"/>
</head>
<body>
<section epub:type="chapter">
<h1>Chapter 1: Introduction</h1>
<p>Text here.</p>
</section>
</body>
</html>
Navigation: NCX vs Nav Document
EPUB has two generations of table-of-contents file. The NCX (Navigation Control for XML, from the DAISY Consortium) was the EPUB 2 format — a custom XML file, usually toc.ncx, made of nested navPoint elements. EPUB 3 declared it obsolete and replaced it with the navigation document: an ordinary XHTML file whose <nav> elements are typed with epub:type.
<!-- toc.ncx (EPUB 2) -->
<navMap>
<navPoint id="np1" playOrder="1">
<navLabel><text>Chapter 1</text></navLabel>
<content src="chapter01.xhtml"/>
</navPoint>
</navMap>
<!-- nav.xhtml (EPUB 3) -->
<nav epub:type="toc" id="toc">
<h1>Contents</h1>
<ol>
<li><a href="chapter01.xhtml">Chapter 1</a></li>
<li><a href="chapter02.xhtml">Chapter 2</a></li>
</ol>
</nav>
<nav epub:type="landmarks" hidden="">
<ol>
<li><a epub:type="toc" href="#toc">Table of Contents</a></li>
<li><a epub:type="bodymatter" href="chapter01.xhtml">Begin Reading</a></li>
</ol>
</nav>
| Feature | NCX (EPUB 2) | Nav document (EPUB 3) |
|---|---|---|
| File format | Custom XML | Standard XHTML, readable in a browser |
| Nested TOC | Nested navPoint | Nested ol |
| Page list | pageTarget | nav epub:type="page-list" |
| Landmarks | navList | nav epub:type="landmarks" |
| Accessibility | Limited | Full ARIA and WCAG support |
| Status | Obsolete, still permitted | Required in EPUB 3 |
The nav document is required in EPUB 3 and read by every modern reader (Kindle, Kobo, Apple Books, Thorium, Calibre). The NCX is optional but harmless: include it for first-generation Kindles and some library systems. Declare it in the manifest with media-type="application/x-dtbncx+xml" and reference it with <spine toc="ncx">; EPUBCheck warns about it in strict EPUB 3 mode but does not fail. When both are present, EPUB 3 readers use the nav document and EPUB 2 readers fall back to the NCX. The optional landmarks nav gives screen readers jump points (cover, toc, bodymatter, backmatter); the page-list nav is covered under pagination below.
Scripting: JavaScript in EPUB 3
EPUB 3 allows JavaScript, but with much tighter limits than the web. A content file that contains scripts must be flagged in the manifest, and inline scripts must be wrapped in CDATA so the XML parser does not choke on < and & operators:
<!-- content.opf -->
<item id="ch1" href="chapter01.xhtml" media-type="application/xhtml+xml"
properties="scripted"/>
<!-- chapter01.xhtml -->
<button id="toggle-btn" type="button" aria-expanded="false" aria-controls="answer">Show answer</button>
<div id="answer" hidden=""><p>The answer is 42.</p></div>
<script>
//<![CDATA[
document.getElementById('toggle-btn').addEventListener('click', function () {
var panel = document.getElementById('answer');
panel.hidden = !panel.hidden;
this.setAttribute('aria-expanded', String(!panel.hidden));
});
//]]>
</script>
| Reading system | JavaScript |
|---|---|
| Apple Books | Yes — best support, full DOM access |
| Thorium Reader, Calibre viewer | Yes — Chromium-based, good for testing |
| Kindle app | Partial — avoid complex DOM manipulation |
| Kindle and Kobo e-ink devices | No — scripts silently ignored |
| Adobe Digital Editions | No — scripts stripped |
Because most e-ink devices ignore scripts, a scripted EPUB must degrade gracefully: keep all content in the HTML and let scripts enhance it, use the hidden attribute rather than display:none so content is reachable without JavaScript, announce dynamic changes with aria-live, and test with scripting disabled. Scripts cannot make network requests, rely on localStorage, touch the filesystem or device APIs, or communicate between spine items — each XHTML file runs in isolation.
Pagination: Page Breaks and Page Numbers
EPUB is reflowable — there are no fixed pages, and "page 42" depends on the screen and font size. Reading systems still paginate, and two mechanisms let authors control that: CSS break properties decide where a screen page may end, and static page markers record where the print pages began.
CSS page breaks
Use both the modern and legacy property names — Kindle historically honours page-break-before: always more reliably than break-before: page:
h1.chapter-title { page-break-before: always; break-before: page; }
table, figure { page-break-inside: avoid; break-inside: avoid; }
h2, h3 { page-break-after: avoid; break-after: avoid; } /* keep heading with next paragraph */
p { orphans: 3; widows: 3; } /* min lines at page bottom / top */
Kobo, Apple Books, Thorium and Calibre support all of these; Kindle supports the legacy properties, only partially supports break-inside: avoid, and ignores orphans/widows. The most reliable chapter break is not CSS at all: put each chapter in its own XHTML file in the spine, because every reading system starts a new spine item on a fresh page. CSS breaks only apply to block elements — to break mid-paragraph, split it into two <p> elements.
Print page numbers: pagebreak markers and the page-list nav
To keep print page references usable in reflowable text — for academic citation, cross-referencing a print edition, or EPUB Accessibility 1.1 conformance for books that have a print equivalent — insert an invisible marker where each print page began, and list them in a page-list nav:
<!-- in the chapter, where print page 47 begins -->
<span epub:type="pagebreak" role="doc-pagebreak" id="page47" aria-label="Page 47"></span>
<!-- nav.xhtml -->
<nav epub:type="page-list" hidden="">
<ol>
<li><a href="chapter01.xhtml#page1">1</a></li>
<li><a href="chapter02.xhtml#page47">47</a></li>
</ol>
</nav>
Screen readers announce the aria-label, and reading systems that support the page list offer "go to page" navigation. The hidden="" attribute keeps the list out of the visual flow. Note that these markers are unrelated to the page counts consumer readers display — Kindle's page numbers are estimated from a standard font size, and most apps show a percentage or time remaining instead.
Inspecting an EPUB
# List contents without extracting
unzip -l book.epub
# Extract to a folder
unzip book.epub -d book_extracted/
# View OPF
unzip -p book.epub OEBPS/content.opf | xmllint --format -
EPUBs from PDF Conversion
When toolkit.bot converts a PDF to EPUB, it generates all required files: mimetype, container.xml, content.opf, a nav document plus a toc.ncx fallback, chapter XHTML files (one spine item per chapter, with page-break CSS on headings and break-inside: avoid on tables and figures), epub:type="pagebreak" markers and a page-list nav at the original PDF page boundaries, embedded images, and a stylesheet. The output passes EPUBCheck validation and includes EPUB Accessibility 1.1 metadata.