レイアウトがスキーマになる

宮下 剛輔

合同会社Serverspec Operations代表社員
フリーランスのソフトウェアエンジニア
mizzy.org / mizzy / gosukenator
北海道出身・神奈川県在住
沖縄は約4年ぶりです

Infrastructure as Code関連の主な著作

2015年にハッカソンだけ参加してました

naoyaさんと相部屋

Peithoとは

Markdownで書いてブラウザで発表する

コンテンツはMarkdown、レイアウトはHTML、見た目はCSS
ビルドすると静的なファイル一式になる
他にあまりない特徴は2種類の検査
静的検査(型検査): レイアウトに書いたスロット定義がそのままスキーマになる
動的検査(lint): 実際に描画して、はみ出しや小さすぎる文字を報せる

開発の動機

プレゼンツール利用遍歴

1997年 〜 2014年
PowerPoint / Keynote
2014年 〜 2025年
Deckset
2025年
k1LoW/deck

Deckset

Markdownで内容を書き、デザインはプリセットのテーマから選択
色やフォントはカスタマイズできるが、レイアウトは変えられない
オリジナルテーマの作成はできない
デザインのことを考えずに中身に集中できるのは利点
レイアウト調整ができない、特にコードを見やすくするための調整ができないのが難点

k1LoW/deck

Markdownで書いた内容をGoogle Slidesに流し込む
デザインはGoogle Slidesのレイアウトとして自分でつくれる
HTML/CSSを書かなくてもGoogle SlidesのUIでデザインを調整できるのは自分にはうれしい
Songmu/laminateと組み合わせることでコードも見やすくできる
シンタックスハイライト+画像化が簡単にできる

最高すぎてもう戻れない

と思っていたが…

AIの進歩がすさまじい
自分が苦手なデザインをAIに任せたいと思うようになった
Google SlidesはAIにデザインを任せるには不便
AIにデザインを任せやすいプレゼンツールが欲しくなった
ただしコンテンツの主導権は自分が握りたい
だから自分でつくることにした

設計の過程

デザインの記述はHTML/CSS

「Claude Designにスライドをデザインしてもらうなら、フォーマットは何がベスト?」
答えはHTML/CSS。Claude Designが生成するのがHTML/CSSだから
当初Speaker Deckへの入稿(PDF)も考慮
Speaker Deckにはこだわらないことにした

コンテンツの記述はMarkdown

コンテンツとデザインの分離
書いている間、内容に集中できる
バージョン管理したい
既存ツールはどれもMarkdown
Deckset・k1LoW/deck・Marp・Slidev...etc

成果物をどう生成するか

最初に考えたのは「Markdownを書く → Claude Designが完成したスライドのHTMLにする → Peithoで発表する」
だがAIが完成品を吐くならPeithoの役割は発表だけになる
それならreveal.jsで十分ではないか、という話になる
そもそもAIの生成は同じ入力から同じ出力になるとは限らない
Markdown修正 → 生成 → 確認のループを回す時間も気になる
そこでHTMLはPeithoが決まったルールで生成し、AIを通さない

役割の決定

Markdown
内容と構造を記述する
Peitho
MarkdownをレイアウトHTMLに流し込み、発表まで担う
Claude Design
スライドの生成には挟まず、テーマとレイアウト体系をつくる
それをClaude CodeがレイアウトHTMLとCSSに書き起こす

MarpやSlidevとの差別化

HTMLネイティブでMarkdownから組み立てるツールは既にある
ではPeithoの差別化をどこに置くか
レイアウトに型を持たせて内容を検査する

型を持ち込むのはClaudeの提案

Carinaで独自DSLと型システムをやってるmizzyさんなら、スライド記法そのものに型や構造の保証を持ち込む(例: レイアウトの制約を型で縛る、参照切れをコンパイル時に検出する)あたりが、既存ツールにないPeithoらしさになりうる

k1LoW/deckのプレースホルダを型の土台にした

k1LoW/deckが使うGoogle Slidesのレイアウトは、名前⁠付き​プレースホルダ(TITLE / SUBTITLE / BODY)を持つテンプレート
Markdownは決まったルールでプレースホルダに流し込まれる
Peithoはこのプレースホルダに型を持たせることにした

Google Slidesのレイアウトとプレースホルダの例

k1LoW/deckのMarkdown記法

前ページのレイアウトに流し込むMarkdown

<!-- {"layout": "title-subtitle-image"} -->

# スライドのタイトル

## サブタイトル

![](image.png)

黙って捨てずにビルドエラーにする

プレースホルダが足りない場合、残りのコンテンツは描画されない
— k1LoW/deckのREADME
k1LoW/deckではプレースホルダに対する検査がない
Peithoではプレースホルダをスロットと呼び、受け取る型と個数を宣言する
合わなければ流し込む前にビルドエラーにする

静的検査(型検査)

レイアウトがスキーマになる

スロットが受け取る型と個数をレイアウトHTMLの中で宣言する。これが契約になる

<section>
  <h1><slot name="title"
        accepts="inline" arity="1"></slot></h1>
  <slot name="code"
        accepts="code" arity="0..1"></slot>
</section>

満たす例

満たさない例

受け取るスロットがないとビルドエラーになる

本文を書くとこうなる

error: slide 1 ('slide-1'), line 7:
  no slot in layout 'simple' accepts this paragraph
 help: add a 'body' slot to the layout or remove the paragraph

個数が合わなければビルドエラーになる

コードブロックを2つ書くとこうなる

error: slide 1 ('slide-1'), line 7:
  slot 'code' got 2 item(s), but layout 'simple' allows 0..1
 help: use a layout with more code capacity
   or remove one code block

契約に合うレイアウトが自動で選ばれる

レイアウト名の指定は省略できる
Peithoがレイアウトを決める手順
明示指定{"layout":"…"}があればそれを使う。未知の名前はビルドエラー
明示指定がなければスライドの内容を各レイアウトの契約に当てる
適合がちょうど1つならそれに決まる
複数でも0個でもビルドエラー。黙ってどれかを選ぶことはしない

合うレイアウトがなければビルドエラーになる

合うレイアウトが存在しないとこうなる

error: slide 1 ('slide-1'), line 5:
  no layout matches this slide
  shot: no slot in layout 'shot' accepts this code block
  simple: no slot in layout 'simple' accepts this image
 help: adjust the slide content,
   or add a layout that accepts it

曖昧ならビルドエラーになる

契約に合うレイアウトが3つある場合、こうなる

error: slide 1 ('slide-1'), line 5:
  slide matches multiple layouts:
  chapter, cover, statement
 help: pick one explicitly
   with <!-- {"layout":"…"} -->

名前を間違えたら候補が出る

レイアウト名をcodeではなくkodeと書くとこうなる

error: slide 1 ('slide-1'), line 5:
  unknown layout 'kode'
 help: use one of: books, chapter, code-stack,
   code, compare, cover, photos, portrait,
   profile, shot, statement, topic

検査はCSSにも適用される

存在しないキーを指すとビルドエラーになる

キーを付けたスライドを消してCSSだけ残すとこうなる

error: line 7: overrides.css:
  unknown slide key 'whoami'
  in override selector
 help: add "key":"whoami" to the page
   settings comment of the slide it
   styles (<!-- {"key":"whoami"} --> if
   it has none), or delete this rule
   from overrides.css

動的検査(lint)

動的検査は型検査のあとで思いついた

型検査は最初の設計段階からあった
はみ出しの検査を2週間後に追加
入れてみたらPeithoのサンプルスライドで2枚はみ出していた
さらに2週間後、文字サイズの検査を追加
はみ出しを文字を縮めてごまかせないようにする

動的検査がやっていること

ビルドした全スライドをheadless Chromeで描画する
はみ出し
スライドの枠: 全要素のgetBoundingClientRect()と比べる
要素の枠: 中身を切る要素(overflow: hiddenなど)のscrollHeight - clientHeight
文字サイズ: getComputedStyle(要素).fontSizeの最小値を見る

スライドの枠からのはみ出しの測り方

スライドの枠からはみ出した場合

中身がスライドの枠を超えるとこうなる

warning: slide 1 content overflows
  the slide box vertically by 257px
  (content 977px, box 720px)
   help: shrink or split the slide content,
   or adjust the layout CSS
checked 1 slide(s): 1 warning(s)

要素の枠からのはみ出しの測り方

要素の枠からはみ出した場合

コードがパネルに収まらないとこうなる

warning: slide 1 content overflows
  the `code` slot vertically by 95px
   help: shrink or split the slide content,
   or adjust the layout CSS
checked 1 slide(s): 1 warning(s)

文字が小さい場合

文字が24ptより小さいとこうなる

warning: slide 1 has text at 12pt,
  below the recommended 24pt: "行1 ダミー…"
   help: raise the font size in the layout CSS,
   or move content to another slide
   instead of shrinking it

わざと小さくするならCSSで宣言する

なぜ描画して測るのか

CSSを静的に読んでも実際の大きさは分からない
カスケード、継承、emや%のような相対単位がある
何行になるかは描いてみないと分からない
折り返す位置はフォントの字幅によって変わる
描画してもすぐには大きさが確定しない
画像とWebフォントは読み込みが終わると再描画される
動的検査は読み込みを待ってから測る

まとめ

Peithoは型のあるプレゼンツール

静的検査(型検査): 内容がレイアウトの型に合うかを確かめる
合わなければ黙って捨てずにビルドエラーにする
動的検査(lint): 型では見つからない崩れを補う
はみ出しや小さすぎる文字を描画して測る

Peithoの柱は2つのプロダクトの掛け合わせ

コンテンツとデザインを分離する
k1LoW/deckから
Markdownをレイアウトのプレースホルダに流し込む
k1LoW/deckから
型で契約を縛り、合わなければエラーにする
Carinaから

ありがとうございました

github.com/mizzy/peitho
peitho.gosu.ke(ドキュメントとデモ)
brew install mizzy/tap/peitho