WEELWORKS

表紙・目次・改ページ。納品できる体裁と、複数文書の一括ビルド

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 側に対応する定義が無いと、この指定はまったく効きません。

共通フロントマターも CSS も入れていない PDF。全1ページ

上は前回までの到達点にあたる 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

共通フロントマターと CSS を入れた PDF。全5ページを3列×2行に並べたもの

5ページになりました。左上から順に、表紙、更新履歴、目次、「はじめに」、「システム構成」です。両方の画像は同じ縮尺です。

ただしこの2枚の差は、2つの変更が重なった結果です。前3ページ(表紙・更新履歴・目次)は、今回 include した共通フロントマターが生んだ中身です。後ろ2ページが章ごとに分かれているところが、CSS の改ページ指定の効果です。CSS だけでは表紙は生えず、フロントマターだけでは紙面は分かれません。

#header を落とさないとタイトルが二重に表示されます。表紙を自分で組んでも、Asciidoctor は文書ヘッダー(タイトル・著者・版)を自動生成するためです。

境界ごとに <<< を1つだけ置く素直な書き方で、余計な空白ページは入りません。

改ページのやり方は <<< だけではありません。CSS で見出し要素へ break-before: page を当てれば、章の先頭で自動的に改ページできます。原稿のどこで切るかを自分で決めたいなら <<<、章はすべて分けると決めているなら CSS が向いています。

:doctype: book を付ける手もあります。テーマ側がタイトル直後で改ページしてくれますが、どこで切るかはテーマ任せになります。改ページの位置を自分で決めたい場合は、先の2つのほうが扱いやすいでしょう。

複数の文書を一括でビルドする

文書は1本では終わりません。手順はどれも同じ2コマンドなので、まとめて回せるようにします。

_common/frontmatter.adocspec-a/index.adocspec-b/index.adocdest/spec-a/spec-a.pdfdest/spec-b/spec-b.pdfincludeincludeasciidoctor → vivliostyleasciidoctor → vivliostyle

文書1本あたりのコマンドは次の2行です。構成は第2回・第3回と同じ asciidoctor@4.0.7 / asciidoctor-kroki@1.0.1 / @vivliostyle/cli@11.1.0 です。

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.ymltaskfile.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.jsonscripts で同じことができます。

{
  "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 が要りません。そして && 連結なので逐次実行になります。taskdeps は並行、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 でタスクを定義するタスクランナー