AIで作ったEPUB商品を販売前に検査する|目次・欠落・リンクの3件を止めた

AIで作ったEPUB商品を販売前に検査する|目次・欠落・リンクの3件を止めた

目次は見えます。本文も開けます。それでも、その EPUB は販売候補にできませんでした。

2026 年 10 月 1 日、AI で下書きしたプロンプト集を EPUB へ固めました。最初の検査では、目次の宣言漏れ、存在しない章、未定義リンクの 3 件が見つかりました。見た目だけを確認していたら、少なくとも 2 件は読み飛ばしていました。

修正後は、同じ EPUBCheck 5.4.0 でエラー0 件です。検査規則を緩めず、package.opfと XHTML を直しました。

前回完了時から変えたこと

前回は、AI で作った JSON 商品台帳を検査しました。価格と公開状態の型違い、同梱ファイルの欠落を止めています。今回は台帳ではなく、購入者が読む EPUB そのものを入力へ渡します。

時点確認できること残る問題
前回完了時JSON台帳の型と同梱ファイルを照合しますEPUB内の目次、章、リンクは見ません
今回完了後EPUBの構造と参照先を公式チェッカーで確認します読みやすさと端末ごとの表示は別に確認します

読み飛ばせる章: EPUBのmanifest、spine、navigation documentを知っている場合は、「3件の不備を入れたEPUBを作る」から進めます。

なぜZIPとして開くだけでは足りないのか

EPUB は、XHTML や画像などを専用の ZIP コンテナへまとめた形式です。拡張子を.epubへ変えるだけでは、読む順番や目次の役割は伝わりません。

W3C の EPUB 3.4 仕様では、package document がメタデータ、manifest、spineを持ちます。manifestは収録資源を列挙し、spineは標準の読書順を示します。navigation document は、目次などの移動手段を担います。

AI へ複数ファイルを別々に書かせると、境界で不一致が起きます。

  • nav.xhtmlを作っても、package.opfでnavと宣言していません
  • manifestには章があるのに、EPUB 内へ収録していません
  • XHTML のリンク先に、対応するidがありません

各ファイルは XML として読めます。しかし、EPUB 全体では参照がつながっていません。

販売前の流れを次の形にしました。

[AIで本文・目次・package documentを下書き]
                    │
                    ▼
[PythonでEPUBコンテナを作成]
                    │
                    ▼
[EPUBCheckで構造と参照を検査]
             ┌──────┴──────┐
             ▼             ▼
        errors=0        errors>0
             │             └── 対象ファイルを修正
             ▼
[実際の読書アプリで表示を確認]
                    │
                    ▼
              [販売候補]

EPUBCheck の合格は、自動販売の許可ではありません。内容の価値、文章の読みやすさ、端末ごとの表示は、人が別に確認します。

用語をそろえる

用語この回での意味
EPUBXHTMLなどの資源を専用ZIPコンテナへまとめた電子出版形式です
package document書誌情報、収録資源、読書順を記録するpackage.opfです
manifestEPUBへ含める資源を列挙する一覧です
spine読者が進む標準の順番を示す一覧です
navigation document目次などの移動手段を持つXHTMLです
fragment identifierURLの#prompt-listのように、文書内の位置を指す識別子です
EPUBCheckEPUBの仕様適合を検査する公式チェッカーです
終了コード合格を0、不合格を0以外で伝える値です

EPUBCheck 5.4.0を固定する

今回使った EPUBCheck は 5.4.0 です。2026 年 9 月 15 日に公開された production-ready 版で、EPUB 3.4 の Candidate Recommendation に対応しています。

実験場所で配布 ZIP を取得し、Python のzipfileコマンドで展開しました。Java は mise 経由の Temurin 21 を使います。

mkdir -p hibanas-epub-preflight
cd hibanas-epub-preflight
curl -fsSLO https://github.com/w3c/epubcheck/releases/download/v5.4.0/epubcheck-5.4.0.zip
python3 -m zipfile -e epubcheck-5.4.0.zip tools
mise x java@temurin-21 -- java -jar tools/epubcheck-5.4.0/epubcheck.jar --version
EPUBCheck v5.4.0

releases/latestだけに頼らず、記事と自動検査ではv5.4.0を明示します。版が変わると、対応仕様やメッセージが変わるためです。

3件の不備を入れたEPUBを作る

Python 3.13.5 で、初稿と修正版を同時に作りました。

python3 --version
Python 3.13.5

保存したbuild_epub_sample.pyは次の内容です。

from __future__ import annotations

from pathlib import Path
import zipfile

ROOT = Path("hibanas-epub-lab")
FIXED_TIME = (2026, 10, 1, 14, 0, 0)

MIMETYPE = "application/epub+zip"
CONTAINER = """<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>
"""
NAV = """<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="ja" xml:lang="ja">
<head><title>目次</title></head>
<body><nav epub:type="toc"><h1>目次</h1><ol><li><a href="chapter.xhtml">はじめに</a></li></ol></nav></body>
</html>
"""
UNSAFE_CHAPTER = """<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
<head><title>はじめに</title></head>
<body><h1>はじめに</h1><p>顧客ヒアリング用AIプロンプト集です。</p><p><a href="#prompt-list">質問一覧へ移動</a></p></body>
</html>
"""
SAFE_CHAPTER = UNSAFE_CHAPTER.replace(
    "<h1>はじめに</h1>", '<h1 id="prompt-list">はじめに</h1>'
)
UNSAFE_OPF = """<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id" xml:lang="ja">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="pub-id">urn:uuid:44cf3d4a-5e77-48dd-af8c-43da77007443</dc:identifier>
    <dc:title>顧客ヒアリング用AIプロンプト集</dc:title>
    <dc:language>ja</dc:language>
    <meta property="dcterms:modified">2026-10-01T14:00:00Z</meta>
  </metadata>
  <manifest>
    <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml"/>
    <item id="chapter" href="chapter.xhtml" media-type="application/xhtml+xml"/>
    <item id="missing" href="missing.xhtml" media-type="application/xhtml+xml"/>
  </manifest>
  <spine>
    <itemref idref="chapter"/>
  </spine>
</package>
"""
SAFE_OPF = UNSAFE_OPF.replace(
    '<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml"/>',
    '<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>',
).replace(
    '    <item id="missing" href="missing.xhtml" media-type="application/xhtml+xml"/>\n',
    "",
)


def member(name: str, stored: bool = False) -> zipfile.ZipInfo:
    info = zipfile.ZipInfo(name, FIXED_TIME)
    info.compress_type = zipfile.ZIP_STORED if stored else zipfile.ZIP_DEFLATED
    return info


def build(name: str, opf: str, chapter: str) -> None:
    output = ROOT / name
    with zipfile.ZipFile(output, "w") as archive:
        archive.writestr(member("mimetype", stored=True), MIMETYPE)
        archive.writestr(member("META-INF/container.xml"), CONTAINER)
        archive.writestr(member("EPUB/package.opf"), opf)
        archive.writestr(member("EPUB/nav.xhtml"), NAV)
        archive.writestr(member("EPUB/chapter.xhtml"), chapter)
    print(f"wrote={output}")


ROOT.mkdir(parents=True, exist_ok=True)
build("prompt-pack-unsafe.epub", UNSAFE_OPF, UNSAFE_CHAPTER)
build("prompt-pack-safe.epub", SAFE_OPF, SAFE_CHAPTER)

初稿には、意図的に次の 3 件を残しました。

  1. nav.xhtmlへproperties="nav"を付けていません
  2. missing.xhtmlをmanifestへ載せましたが、実ファイルは作りません
  3. #prompt-listへのリンクがありますが、対応するidがありません

修正版は、目次へnavの役割を付けます。存在しない章を一覧から外し、見出しへid="prompt-list"を追加します。

Pythonで2つのEPUBを組み立てる

作成スクリプトを実行しました。

python3 build_epub_sample.py
wrote=hibanas-epub-lab/prompt-pack-unsafe.epub
wrote=hibanas-epub-lab/prompt-pack-safe.epub

Python のzipfileは、ZIP の作成、読み込み、一覧表示を扱えます。EPUB では通常の ZIP より 1 つ制約が増えます。mimetypeを先頭に置き、圧縮しない状態で格納します。

コードでは、最初のwritestr()にZIP_STOREDを指定しました。残りの XHTML と XML にはZIP_DEFLATEDを使います。

修正版の収録順も確認しました。

python3 -m zipfile -l hibanas-epub-lab/prompt-pack-safe.epub
File Name                                             Modified             Size
mimetype                                       2026-10-01 14:00:00           20
META-INF/container.xml                         2026-10-01 14:00:00          251
EPUB/package.opf                               2026-10-01 14:00:00          736
EPUB/nav.xhtml                                 2026-10-01 14:00:00          306
EPUB/chapter.xhtml                             2026-10-01 14:00:00          323

mimetypeが先頭です。ただし、この一覧だけではmanifestの役割や XHTML 内リンクまでは検査できません。

初稿を検査して3件で止める

初稿を EPUBCheck へ渡し、終了コードも表示しました。

mise x java@temurin-21 -- java -jar tools/epubcheck-5.4.0/epubcheck.jar \
  hibanas-epub-lab/prompt-pack-unsafe.epub
printf 'exit=%s\n' "$?"
Validating using EPUB version 3.4 rules.
ERROR(RSC-005): hibanas-epub-lab/prompt-pack-unsafe.epub/EPUB/package.opf(9,13): Error while parsing file: Exactly one manifest item must declare the "nav" property (number of "nav" items: 0).
ERROR(RSC-001): hibanas-epub-lab/prompt-pack-unsafe.epub/EPUB/missing.xhtml(-1,-1): File "EPUB/missing.xhtml" could not be found.
ERROR(RSC-012): hibanas-epub-lab/prompt-pack-unsafe.epub/EPUB/chapter.xhtml(4,72): Fragment identifier is not defined.

Check finished with errors
Messages: 0 fatals / 3 errors / 0 warnings / 0 infos

EPUBCheck completed
exit=1

3 件は別の境界にあります。

  • RSC-005は、目次ファイルの存在ではなく役割の宣言を見ています
  • RSC-001は、manifestの記述とコンテナ内の実ファイルを照合しています
  • RSC-012は、XHTML 内のリンクと対応する識別子を照合しています

nav.xhtmlを削れば、目次のエラーは見えなくなります。しかし、購入者の移動手段も失います。検査件数を減らすのではなく、目次の役割を正しく宣言します。

同じ理由で、missing.xhtmlを空ファイルとして足しません。今回は収録予定のない項目だったため、manifestから外しました。必要な章なら、本文を作って収録する方を選びます。

3か所を直して同じ検査へ戻す

修正版では、検査条件と EPUBCheck の版を変えていません。

mise x java@temurin-21 -- java -jar tools/epubcheck-5.4.0/epubcheck.jar \
  hibanas-epub-lab/prompt-pack-safe.epub
printf 'exit=%s\n' "$?"
Validating using EPUB version 3.4 rules.
No errors or warnings detected.
Messages: 0 fatals / 0 errors / 0 warnings / 0 infos

EPUBCheck completed
exit=0

3 errorsから0 errorsへ変わりました。初稿と修正版の差は次の 3 点です。

初稿修正版意図
nav.xhtmlに役割がありませんproperties="nav"を追加しますnavigation documentを一意に示します
missing.xhtmlを列挙しています不要なitemを削除しますmanifestと収録物を一致させます
#prompt-listだけがあります見出しへ同じidを付けます文書内リンクの着地点を作ります

検査した版をハッシュで固定する

初稿、修正版、作成スクリプトの SHA-256 を残しました。

sha256sum \
  hibanas-epub-lab/prompt-pack-unsafe.epub \
  hibanas-epub-lab/prompt-pack-safe.epub \
  build_epub_sample.py
0e73db3a20838d525d4b759f6e16473771bc02a6bd57971812532844b8c8c4d2  hibanas-epub-lab/prompt-pack-unsafe.epub
f06b2cd1280cb5120e0c0c28ac180687321cc14473f0477b99f4ed2bcfba2d0b  hibanas-epub-lab/prompt-pack-safe.epub
f244d4c6c64e55c2120234bff582063114ffc8fa1f8d537ca145bc8f57c0da6e  build_epub_sample.py

スクリプトでは、ZIP 内の日時を固定しています。同じ内容を同じ順序で格納すれば、比較しやすいファイルになります。

ハッシュは、検査した版と販売候補が同じかを確認する値です。EPUB の内容が良いことや、安全であることまでは証明しません。

EPUBCheck合格後に残る確認

exit=0でも、商品として完成したとは言えません。今回の検査が見たのは仕様適合です。

私は、販売候補を次の 3 段で止めます。

  1. EPUBCheck で構造、資源、参照先を検査します
  2. 2 種類以上の読書アプリで目次、改ページ、文字サイズを確認します
  3. 購入者の手順で取得し、最初から最後まで読みます

2 段目が必要なのは、仕様適合と表示の使いやすさが同じではないためです。長いコード、表、縦書き、埋め込みフォントは、端末差が出やすい部分です。

3 段目では、販売ページから取得した完成ファイルを使います。作業フォルダの EPUB が合格しても、アップロード時に古い版を選ぶ事故は残ります。ハッシュは、その取り違えを見つける材料になります。

よくあるエラー

症状原因対応
RSC-005でnavが0件と出ます目次XHTMLを作っただけで、manifestへ役割を付けていません対象itemへproperties="nav"を付けます
RSC-005でnavが複数と出ます2つ以上のitemへnavを付けていますpublication全体のnavigation documentを1つに決めます
RSC-001でファイルがないと出ますmanifestとEPUB内の実ファイルが一致しません必要なら収録し、不要なら一覧から外します
RSC-012でfragmentが未定義と出ます#idのリンク先に同じidがありません着地点へ一意なidを追加します
ZIPとして展開できますが検査に落ちますZIPの成立とEPUBの仕様適合を混同していますEPUBCheckへ完成ファイルを渡します
手元では合格しますが販売物が古いままです検査後に別ファイルをアップロードしていますSHA-256を照合し、完成物を再検査します
合格しても目次が使いにくいです仕様適合だけを確認しています実際の読書アプリで移動順と表示名を確認します
版を上げたら新しいメッセージが出ます対応仕様と検査規則が更新されています版を記録し、新規メッセージを確認してから固定します

失敗を3件のまま残した理由

AI へ「正しい EPUB を作って」と頼み直すだけでは、何が直ったか分かりません。今回は、初稿を消さずに修正版と並べました。

3 件のうち、文章の誤字は 1 件もありません。すべてファイル間の接続です。生成した本文だけを校正しても、package.opfと XHTML のずれは残ります。

個人でデジタル商品を作ると、執筆、梱包、販売準備を同じ人が進めます。AI で各工程が速くなるほど、境界の確認が後回しになります。EPUBCheck を販売直前へ置くと、速く作れたファイルをそのまま出さずに済みます。

次に広げるなら、画像の代替テキストと EPUB Accessibility のメタデータです。ただし、検査項目を一度に増やしません。今回の 3 件を確実に止めた上で、読書アプリの実表示とアクセシビリティ確認を別の実験として足します。

一次情報

資料確認した内容
W3C: EPUB 3.4EPUBコンテナ、package document、manifest、spine、navigation documentの役割
W3C EPUBCheck v5.4.0 release5.4.0がEPUB 3.4 Candidate Recommendationに対応するproduction-ready版であること
W3C: EPUBCheckEPUBCheckがEPUBの公式conformance checkerであり、コマンドラインで実行できること
Python 3.14: zipfileZipFile、writestr()、ZIP_STORED、ZIP_DEFLATED、一覧表示の扱い
この記事をシェア