表紙・目次・改ページ。納品できる体裁と、複数文書の一括ビルド
PDF は出るようになり、図も本文と同じソースから生成できます。残っているのは、他人に渡せる形に仕上げることです。今回は表紙と更新履歴と目次を付け、章ごとに改ページし、複数の文書をまとめてビルドします。連載の最終回です。
「納品できる体裁」とは何か
装飾の話ではありません。仕様書を渡す相手が必要としているのは、次の4つです。
- 表紙:何の文書で、どのプロジェクトのもので、誰が、いつ、どの版を出したのか
- 更新履歴:版数・日付・変更内容の一覧
- 目次:全体の構造と、目当ての章の位置
- 章ごとの改ページ:章の境界が紙面の境界と一致していること
章が紙面の途中から始まっていれば「4章の頭を見てください」は通じませんし、更新履歴が無ければ「前回と何が変わったのか」に答えられません。体裁は見た目ではなく、成果物の構造の問題です。
ページ番号も本来この一覧に入りますが、これは CSS の @page が受け持つ領域なので、この連載では扱いません。
表紙・更新履歴・目次を共通ファイルにまとめる
表紙も更新履歴も目次も、文書ごとに違うのは内容だけで形式は共通です。そこで形式を1ファイルに置き、内容は各文書の属性で差し替えます。
project/
├── docs/
│ ├── _common/
│ │ └── frontmatter.adoc
│ ├── spec-a/
│ │ ├── index.adoc
│ │ └── content-body.adoc
│ └── spec-b/
│ ├── index.adoc
│ └── content-body.adoc
├── style.css
└── Taskfile.yml
共通フロントマターの全文です。値はすべて {属性名} の参照で、固有名詞はひとつも書かれていません。
[.cover]
== {doc-name}
{project-name}
製作者: {author}
更新日: {revdate}
版数: v{revnumber}
<<<
== 更新履歴
|===
|版数 |日付 |内容
|v{revnumber} |{revdate} |初版
|===
<<<
== 目次
toc::[]
<<<
冒頭の [.cover] はロール指定で、そのセクションに cover という class が付きます。表紙のスタイルを CSS から扱うためのセレクタとなります。
各文書の index.adoc は、属性を定義してこれを取り込むだけです。
= サンプル仕様書
:toc: macro
:project-name: サンプルプロジェクト
:doc-name: サンプル仕様書
:author: 山田太郎
:revdate: 2026-08-02
:revnumber: 1.0
include::../_common/frontmatter.adoc[]
include::content-body.adoc[]
属性名は、共通フロントマターが参照している名前と一致させてください。第2回のサンプルでは :projectname: と書いていましたが、ここでは {project-name} を参照しているので :project-name: にしています。名前がずれていてもエラーにはならず、表紙に {project-name} という文字列がそのまま出るだけなので、気づきにくい失敗です。
:toc: macro が要点です。この指定だと目次は自動挿入されず、toc::[] と書いた位置――共通フロントマターの中の狙った場所にだけ出ます。include:: の相対パスは第3回のとおり .adoc の場所が基準なので、docs/spec-a/index.adoc からは ../_common/ で届きます。
新しい文書で書くのは属性5行と本文だけです。更新履歴の初版行も {revnumber} と {revdate} を参照しているので、表紙と版がずれません。
CSS で改ページを制御する
AsciiDoc の <<< は改ページ指定で、上の共通フロントマターでも表紙・更新履歴・目次の区切りに置いてあります。HTML 出力では <div class="page-break"></div> に変換されます。
しかし、CSS 側に対応する定義が無いと、この指定はまったく効きません。

上は前回までの到達点にあたる PDF の全ページです。共通フロントマターを include しておらず、--style も指定していません。表紙も更新履歴も目次も無く、自動生成の文書ヘッダーと2つの章、シーケンス図、末尾の一文が1枚に詰め込まれています。章の切れ目に <<< を書いてあるのに改ページされないのは、既定のスタイルシートに .page-break の定義が無いためです。
必要なのは、次の style.css です。画像を生成したときに実際に使ったものと同じ内容です。
#header {
display: none;
}
.page-break {
break-after: page;
}
.cover {
text-align: center;
margin-top: 30%;
}
.cover h2 {
font-size: 2em;
}
.page-break が <<< を実際の改ページに変え、#header が自動生成の文書ヘッダーを止め、.cover の2つがロール指定を受けて表紙を紙面の中央へ送ります。.cover が無いと表紙の文字は左寄せのまま紙面の先頭に貼り付きます。
そしてビルド時に --style で渡します。
npx vivliostyle build dest/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/sample.pdf

5ページになりました。左上から順に、表紙、更新履歴、目次、「はじめに」、「システム構成」です。両方の画像は同じ縮尺です。
ただしこの2枚の差は、2つの変更が重なった結果です。前3ページ(表紙・更新履歴・目次)は、今回 include した共通フロントマターが生んだ中身です。後ろ2ページが章ごとに分かれているところが、CSS の改ページ指定の効果です。CSS だけでは表紙は生えず、フロントマターだけでは紙面は分かれません。
#header を落とさないとタイトルが二重に表示されます。表紙を自分で組んでも、Asciidoctor は文書ヘッダー(タイトル・著者・版)を自動生成するためです。
境界ごとに <<< を1つだけ置く素直な書き方で、余計な空白ページは入りません。
改ページのやり方は <<< だけではありません。CSS で見出し要素へ break-before: page を当てれば、章の先頭で自動的に改ページできます。原稿のどこで切るかを自分で決めたいなら <<<、章はすべて分けると決めているなら CSS が向いています。
:doctype: book を付ける手もあります。テーマ側がタイトル直後で改ページしてくれますが、どこで切るかはテーマ任せになります。改ページの位置を自分で決めたい場合は、先の2つのほうが扱いやすいでしょう。
複数の文書を一括でビルドする
文書は1本では終わりません。手順はどれも同じ2コマンドなので、まとめて回せるようにします。
文書1本あたりのコマンドは次の2行です。構成は第2回・第3回と同じ [email protected] / [email protected] / @vivliostyle/[email protected] です。
npx asciidoctor -b html5 --extension asciidoctor-kroki -o dest/spec-a/index.html docs/spec-a/index.adoc
npx vivliostyle build dest/spec-a/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/spec-a/spec-a.pdf
これを Taskfile.yml(taskfile.dev)にまとめます。検証したのは task のバージョン 3.52.0 です。
version: '3'
tasks:
build:
desc: すべての文書をビルドする
deps:
- build-spec-a
- build-spec-b
build-spec-a:
cmds:
- npx asciidoctor -b html5 --extension asciidoctor-kroki -o dest/spec-a/index.html docs/spec-a/index.adoc
- npx vivliostyle build dest/spec-a/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/spec-a/spec-a.pdf
build-spec-b:
cmds:
- npx asciidoctor -b html5 --extension asciidoctor-kroki -o dest/spec-b/index.html docs/spec-b/index.adoc
- npx vivliostyle build dest/spec-b/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/spec-b/spec-b.pdf
task build を実行すると、deps に並べた2つが並行実行され、両方の PDF が生成されます。
task を入れていない環境なら、package.json の scripts で同じことができます。
{
"scripts": {
"build:spec-a": "asciidoctor -b html5 --extension asciidoctor-kroki -o dest/spec-a/index.html docs/spec-a/index.adoc && vivliostyle build dest/spec-a/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/spec-a/spec-a.pdf",
"build:spec-b": "asciidoctor -b html5 --extension asciidoctor-kroki -o dest/spec-b/index.html docs/spec-b/index.adoc && vivliostyle build dest/spec-b/index.html --theme @vivliostyle/theme-techbook --size A4 --style style.css -o dest/spec-b/spec-b.pdf",
"build": "npm run build:spec-a && npm run build:spec-b"
}
}
違いは2つです。npm scripts では node_modules/.bin が PATH に入るため npx が要りません。そして && 連結なので逐次実行になります。task の deps は並行、npm run は逐次で、文書が増えるほど差が出ます。
なお検証では、task による2文書の並行ビルドと、--style を付けた体裁つきのビルドをそれぞれ別に確認しています。上の「2文書を --style 付きでまとめてビルドする」形を通しで走らせたわけではありません。個々のコマンドは実際に動いたものですが、組み合わせは未確認です。
実務で効くのが、ビルド対象の ON/OFF を切り替えられることです。上の書き方なら deps から1行外すだけで、改訂中の文書を定義ごと消さずに外せます。本数が増えたら、対象の一覧を変数に持たせて回す形へ育てるとよいでしょう。
運用に乗せる
改版で触るのは2か所だけです。index.adoc の :revnumber: と :revdate: を書き換え、更新履歴の表に1行足します。表紙は属性から組まれているので、他は直しません。
この2か所は本文の変更と同じコミットに入れます。 別コミットにすると「版は上がっているが中身が変わっていない」履歴が生まれ、更新履歴が信用されなくなります。
レビューは PDF ではなくプルリクエストの差分で回します。合意が済んでからビルドすれば、PDF は初めて「この版で合意した成果物」になります。PDF はレビューの対象ではなく、レビューの結果です。CI に一括ビルドを載せておけば、ビルドが壊れたこともマージ前に分かります。
そして最初の原則に戻ります。第1回で挙げた「仕様書をコードと同じ土俵に載せる」は、この4回で作ったものの中に全部入っています。本文はプレーンテキストなのでコードと同じリポジトリに置け、差分でレビューできます。図は Kroki のソースとして本文の隣にあり、同じコミットに乗ります。変更の理由はプルリクエストとコミットメッセージに残り、半年後でもたどれます。納品用の体裁は共通ファイルと1枚の CSS が引き受けます。新しい運用ルールをひとつも足さずに、チームがコードですでに回している仕組みをそのまま流用できる――そこが、この構成の狙いでした。
この回で使ったツール
- AsciiDoc — 技術文書向けの軽量マークアップ言語
- Asciidoctor — AsciiDoc を HTML や PDF に変換する処理系
- Vivliostyle — CSS で組版して PDF を生成するツール(テーマ集)
- Task — YAML でタスクを定義するタスクランナー