メインコンテンツへスキップ

BookStackの章・ページ骨格をYAML/JSONから一括生成する(手作業ゼロ運用)

🧭 はじめに

このページでは、BookStackで「本の骨格(章+WIPページ)」を作るときに、章IDを手で拾ってシェルのリテラルを書き換える運用をやめて、YAML/JSONの入力ファイルだけで「章作成→配下ページ作成」まで自動化するワークフローをまとめる。 狙いは次の3点。

  • 入力はYAML/JSONだけ(=章IDの調査・貼り付けを廃止)
  • 章・ページの作成を冪等にする(同名があればスキップ)
  • BookStack API仕様(認証・必須項目・フィルタ)に沿って堅牢にする

ブラウザのDevToolsでAPIを叩く運用は、ログイン済みセッションでもAPIが動くため成立している(ドキュメントにもその挙動が明記されている)。ただし自動化したいならサーバ側はAPIトークンが素直。 :contentReference[oaicite:0]{index=0}


✅ 結論:できる(APIで章もページも作れる)

BookStackのREST APIには以下がある。

  • 章作成POST /api/chaptersbook_idname が必須) (demo.bookstackapp.com)
  • ページ作成POST /api/pagesname 必須、さらに chapter_idbook_id のどちらか必須。本文は htmlmarkdown のどちらか必須) (demo.bookstackapp.com)
  • 一覧取得は GET /api/chapters などで filter[...] が使える(名前で存在確認に使える) (demo.bookstackapp.com)
  • 認証は Authorization: Token <token_id>:<token_secret> ヘッダ (demo.bookstackapp.com)

したがって「YAML/JSON → 章作成 → 子ページ作成」は、curl+jq/yqの範囲で普通に組める。


🧩 新ワークフロー(提案)

🧱 1) YAML/JSONで“骨格”だけを定義する

  • Book(対象の本):book_id
  • 章:name(+任意で description_html など)
  • ページ:name(本文はとりあえずWIPで固定でもいい)

入力ファイルが唯一の真実になるので、「章IDを拾う」「シェルを書き換える」が消える。

⚙️ 2) サーバ上のシェルは入力ファイルを読むだけにする

  • create_skeleton.sh skeleton.yaml のように実行
  • 認証情報は環境変数 or 権限を絞った別ファイルに隔離

TOKEN_ID/TOKEN_SECRETをスクリプトに直書きしない。非公開ページでも、サーバ上の平文は事故る。

🔁 3) 冪等(同名があれば作らない)にして安全運用

  • GET /api/chapters?filter[book_id]=...&filter[name]=... の検索で存在確認(0件なら作成)
  • ページも同様に GET /api/pages?filter[chapter_id]=...&filter[name]=... で確認してから作成 (BookStackのlist APIは datatotal を返すので total==0 判定が楽) (demo.bookstackapp.com)

🗂️ 入力ファイル例(YAML)

skeleton.yaml の例:

book_id: 123

wip:
  html: "<p>WIP</p>"

chapters:
  - name: "01_規格の全体像"
    pages:
      - name: "ISO 26262 / 21434 とA-SPICEの役割分担"
      - name: "A-SPICEを軸にした規格全体設計の考え方"

  - name: "02_PRM/ PAMの読み方"
    pages:
      - name: "PRM(Purpose/Outcome)の実務的な読み替え"
      - name: "PAM(Practice/Work Product)の集め方"

ページ本文は `html` か `markdown` のどちらかが必須(両方不要、はできない)。WIP用に固定の `

WIP

` を使うのが手堅い。 :contentReference[oaicite:6]{index=6}

🧪 スクリプト(YAML入力→章作成→ページ作成)

前提コマンド:

  • curl
  • jq
  • yq(YAML→JSON変換用途。Go版の yq を想定)

yqの方言差がある。ここでは `yq -o=json` が使える系(mikefarah/yq 系)を想定。

create_skeleton.sh

#!/usr/bin/env bash
set -euo pipefail

INPUT_FILE="${1:-}"
if [[ -z "$INPUT_FILE" || ! -f "$INPUT_FILE" ]]; then
  echo "Usage: $0 skeleton.yaml" >&2
  exit 1
fi

: "${BOOKSTACK_BASE_URL:?Set BOOKSTACK_BASE_URL, e.g. https://hiden-no-tare.com}"
: "${TOKEN_ID:?Set TOKEN_ID}"
: "${TOKEN_SECRET:?Set TOKEN_SECRET}"

API_CHAPTERS="${BOOKSTACK_BASE_URL%/}/api/chapters"
API_PAGES="${BOOKSTACK_BASE_URL%/}/api/pages"

auth_header="Authorization: Token ${TOKEN_ID}:${TOKEN_SECRET}"

# YAML -> JSON (yq) -> 変数へ
json="$(yq -o=json "$INPUT_FILE")"

book_id="$(jq -r '.book_id' <<<"$json")"
wip_html="$(jq -r '.wip.html // "<p>WIP</p>"' <<<"$json")"

if [[ "$book_id" == "null" || -z "$book_id" ]]; then
  echo "book_id is required in YAML" >&2
  exit 1
fi

api_get() {
  local url="$1"
  curl -sS -H "$auth_header" -H "Accept: application/json" "$url"
}

api_post_json() {
  local url="$1"
  local payload="$2"
  curl -sS -X POST "$url" \
    -H "$auth_header" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d "$payload"
}

get_or_create_chapter_id() {
  local chapter_name="$1"

  # 既存検索: chapters list は data/total を返す :contentReference[oaicite:7]{index=7}
  local res
  res="$(api_get "${API_CHAPTERS}?filter[book_id]=${book_id}&filter[name]=$(jq -rn --arg s "$chapter_name" '$s|@uri')&count=1")"

  local total
  total="$(jq -r '.total' <<<"$res")"

  if [[ "$total" != "0" ]]; then
    jq -r '.data[0].id' <<<"$res"
    return
  fi

  # 作成: chapter create は book_id と name が必須 :contentReference[oaicite:8]{index=8}
  local payload
  payload="$(jq -n --argjson book_id "$book_id" --arg name "$chapter_name" \
    '{book_id:$book_id, name:$name}')"

  local created
  created="$(api_post_json "$API_CHAPTERS" "$payload")"

  jq -r '.id' <<<"$created"
}

create_page_if_absent() {
  local chapter_id="$1"
  local page_name="$2"

  local res
  res="$(api_get "${API_PAGES}?filter[chapter_id]=${chapter_id}&filter[name]=$(jq -rn --arg s "$page_name" '$s|@uri')&count=1")"

  local total
  total="$(jq -r '.total' <<<"$res")"

  if [[ "$total" != "0" ]]; then
    echo "  - skip page (exists): $page_name"
    return
  fi

  # pages create: name必須、chapter_id or book_id必須、html or markdown必須 :contentReference[oaicite:9]{index=9}
  local payload
  payload="$(jq -n \
    --arg name "$page_name" \
    --arg html "$wip_html" \
    --argjson chapter_id "$chapter_id" \
    '{name:$name, html:$html, chapter_id:$chapter_id}')"

  api_post_json "$API_PAGES" "$payload" >/dev/null
  echo "  - created page: $page_name"
}

# chapters を順に処理
chapters_len="$(jq '.chapters | length' <<<"$json")"
if [[ "$chapters_len" == "0" ]]; then
  echo "No chapters in YAML" >&2
  exit 1
fi

for i in $(seq 0 $((chapters_len - 1))); do
  chapter_name="$(jq -r ".chapters[$i].name" <<<"$json")"
  echo "== chapter: $chapter_name"

  chapter_id="$(get_or_create_chapter_id "$chapter_name")"
  echo "chapter_id=$chapter_id"

  pages_len="$(jq ".chapters[$i].pages | length" <<<"$json")"
  for j in $(seq 0 $((pages_len - 1))); do
    page_name="$(jq -r ".chapters[$i].pages[$j].name" <<<"$json")"
    create_page_if_absent "$chapter_id" "$page_name"
  done
done

echo "Done."

🔐 認証情報の置き場(推奨)

例:~/.config/bookstack/env を作って、実行前に読み込む。

export BOOKSTACK_BASE_URL='https://hiden-no-tare.com'
export TOKEN_ID='XXXXXXX'
export TOKEN_SECRET='YYYYYYY'

実行:

source ~/.config/bookstack/env
./create_skeleton.sh skeleton.yaml

環境変数化しておくと、スクリプトをGit管理しても事故率が下がる。


🧯 よくある落とし穴

⚠️ ページ本文が空だと作成できない

/api/pageshtml または markdown のどちらかが必須。WIPなら固定HTMLを入れるのが一番ラク。 (demo.bookstackapp.com)

⚠️ 同名章・同名ページの扱い

このスクリプトは「同名があればスキップ」方針。 もし同名があり得る運用(例:章名を毎回 “WIP” にする等)だと誤判定するので、プレフィックスに番号を含めるなどで一意性を担保した方がいい。

⚠️ API権限

APIトークンを発行したユーザに “Access System API” 権限が必要。 (demo.bookstackapp.com)

管理者権限ユーザのトークンを使い回さない。骨格生成専用ユーザ(必要最小権限)を作る方が安全。


🧱 拡張案(必要になったら)

  • 章のdescription_htmltags をYAMLに持たせて、そのまま POST /api/chapters に渡す(パラメータはドキュメントに記載あり) (demo.bookstackapp.com)
  • ページ本文を wip.html 固定ではなく、ページごとに html / markdown / file を指定して差し込む
  • “章の並び順” を priority で固定(章もページも priority がある) (demo.bookstackapp.com)

📌 まとめ

  • YAML/JSONを唯一の入力にして、章IDの手作業は全廃できる
  • BookStack APIの「章作成」「ページ作成」「filter検索」を組み合わせれば、冪等な骨格生成がシェルだけで可能 (demo.bookstackapp.com)
  • トークンは直書きせず、環境変数+最小権限が安全

必要なら、次は「JSON入力版(jqだけで完結)」か「ページ本文をファイルから流し込む(Markdownファイル群→BookStack)」版に落としてもいい。