読了時間の目安約 21 分10,082 字
スライドを Markdown で書くと、AI に丸ごと任せられるようになります。
テーマ CSS の自作から Speaker Deck への公開まで。実際に 18 枚のスライドを作った手順を、コードを全部載せて記録します。
スライドを Marp(Markdown をスライドにする仕組み)で書くと、中身もデザインもテキストファイルになります。そのため Claude Code が読んで、直して、出力して、結果を目で確かめるまでを自分で回せるようになります。
PowerPoint や Keynote はバイナリなので、この最後の 2 つができません。そこが決定的な差です。
下に埋め込んだスライドは、この記事で説明する手順でつくったものです。テーマ CSS も生成スクリプトも全文を載せているので、そのままコピーして使えます。
できあがったもの


















表紙
デザインは メルカリのサステナビリティレポートを参照しました。左に章立ての固定レール、見出しの左に縦線、下に進捗バー、というレポート型の構成です。「このURLのデザインを真似して」と URL ごと渡すのが、言葉で説明するよりずっと速いことが分かりました。
Marp とは何か
Markdown をスライドに変換する仕組みです。決まりごとは 1 つだけで、--- で区切った 1 ブロックが 1 スライドになります。あとは普通の Markdown を書けば、HTML・PDF・PNG・PPTX のどれにでも出力できます。
---
marp: true
theme: report
---
# 1枚目
ここが本文になる。
---
# 2枚目
- 箇条書きもそのまま
- 表も画像も使える
これだけで 2 枚のスライドになります。見た目は theme で指定した CSS が決めるので、中身(Markdown)とデザイン(CSS)が完全に分かれているのが特徴です。
なぜ Claude Code と相性がいいのか
理由は 1 つで、スライドがテキストファイルだからです。
| PowerPoint / Keynote | Marp | |
|---|---|---|
| AI が中身を読めるか | 読めない(バイナリ) | 読める |
| 「3枚目の見出しを直して」 | 通らない | 通る |
| 結果を確認できるか | 人間が開くしかない | PNG に出して画像で確認できる |
| 差分が取れるか | 取れない | Git で行単位 |
| 自動化 | 難しい | コマンド 1 本 |
Claude Code は「編集して、出力して、結果を目で確かめる」ループを自分で回せる。バイナリ形式のツールでは、この最後の 2 つができない。
実際、今回のスライドが完成するまでに 生成 → PNG で確認 → 修正 のループを6 回まわしています。直したのは次のようなもので、どれも Markdown を読んでいるだけでは気づけません。
- レールの文字が本文と重なっていた
- コードブロックが枠からはみ出していた
- 中扉のページ番号を消し忘れていた
- 2 カラムの比率が中身に合っていなかった
- 表紙のグラデーションがそもそも表示されていなかった
環境をつくる — 3 つのコマンド
# 1. プロジェクトを作って marp-cli を入れる
mkdir deck && cd deck
npm init -y
npm i -D @marp-team/marp-cli
# 2. 書く
vim slides.md
# 3. PDF にする
npx marp slides.md --pdf
グローバルに入れずプロジェクトごとに入れるのを勧めます。バージョンが固定されるので、他の環境でも同じ結果になります。
PDF が出ないときは、ほぼ Chrome の問題
PDF と PNG の出力には Chrome / Chromium が要ります。見つからないと黙って止まるので、パスを明示的に渡します。
# Playwright の Chromium を使う例
export CHROME_PATH="$(node -e 'const{chromium}=require("playwright");console.log(chromium.executablePath())')"
npx marp slides.md --pdf --browser chrome
--no-stdin を付けるか < /dev/null を渡してください。これで 5 分かかっていた処理が 10 秒になりました。覚えるディレクティブは 4 つだけ
---
marp: true # これがないと動かない
theme: report # 使うテーマ
paginate: true # ページ番号を出す
html: true # HTML を書けるようにする
size: 16:9
---
スライド単位で上書きしたいときは、アンダースコア付きのコメントを書きます。
<!-- _class: cover --> ← このスライドだけ cover クラスにする
<!-- _paginate: false --> ← このスライドだけページ番号を消す
_ を付け忘れると、そのスライド以降すべてに効きます。中扉の背景色を変えたつもりが以降全部が緑になる、というのはこれが原因です。テーマは CSS 1 枚
1 行目の /* @theme 名前 */ がテーマ名になります。あとは普通の CSS で、section が 1 枚のスライドです。
/* @theme report */
:root {
--accent: #12c2a5;
--accent-pale: #eefaf7;
--ink: #1a1d1f;
--rail: 190px; /* 左レールの幅 */
--pad-x: 56px;
}
section {
width: 1280px;
height: 720px;
padding: 42px var(--pad-x) 58px calc(var(--rail) + 46px);
background: #fff;
color: var(--ink);
font-size: 20.5px;
line-height: 1.9;
position: relative;
overflow: hidden;
}
/* 左上のデッキ名 */
section::before {
content: "Claude Code\A で作るスライド";
position: absolute; left: var(--pad-x); top: 42px;
width: calc(var(--rail) - 20px);
color: var(--accent);
white-space: pre-line;
}
/* Marp のページ番号は section::after に入る */
section::after {
content: attr(data-marpit-pagination);
position: absolute; right: var(--pad-x); top: 42px;
font-weight: 700; color: #9aa3a9;
}
h1 {
font-size: 32px;
color: var(--accent);
border-left: 4px solid var(--accent);
padding-left: 18px;
}
読み込ませ方はこうです。
npx marp slides.md --theme-set theme/ --pdf --browser chrome --no-stdin
ハマったところ 3 つ
ここが今回いちばん学びのあった部分です。Marp は入力を素通しするわけではなく、いくつか勝手に加工します。
① content プロパティは section::after から消される
表紙の背景グラデーションを section.cover::after にcontent: "" + background で置いたのですが、表示されませんでした。Marp はページ番号を section::after で描くため、テーマ側の content 宣言を落とします。
解決策は、擬似要素をやめて section 自体の背景に敷くことです。
/* × 効かない */
section.cover::after { content: ""; background: url(...); }
/* ○ 効く */
section.cover {
background-color: #f7f8f8;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...);
background-repeat: no-repeat;
background-position: left bottom;
background-size: 1280px 420px;
}
#12c2a5 の # がCSS のフラグメント記号と解釈されて背景が消えます。これで 2 回やり直しました。② data-* 属性は削られる
番号つきの丸を <li data-n="1"> + content: attr(data-n) で描こうとしたら、丸だけが表示されて数字が出ませんでした。Marp の HTML サニタイザが data-* と一部の style を落とします。
<!-- × 数字が消える -->
<li data-n="1">型を先に作る</li>
<!-- ○ 実体として置く -->
<li><b>1</b>型を先に作る</li>
.steps li > b:first-child {
position: absolute; left: 0; top: 1px;
width: 28px; height: 28px; border-radius: 50%;
background: var(--accent); color: #fff;
display: flex; align-items: center; justify-content: center;
}
進捗バーの幅も style="width:22%" が消えたので、.bar.-p1 〜 .bar.-p18 というクラスを 18 個 CSS に用意して逃げました。class 属性は残ります。
③ コードブロックの中の --- がスライド区切りになる
Marp の使い方を説明するスライドなので、コード例に ---(フロントマターの区切り)が出てきます。これがそのままスライドの区切りとして解釈されて、18 枚のはずが 20 枚になりました。
解決策は、<pre> を物理的に 1 行で出力し、改行を にすることです。
def code(lines):
"""1行のHTMLとして出す。改行を実体参照にしないと、
コード内の --- がスライドの区切りとして解釈されてしまう。"""
out = []
for ln in lines:
e = html.escape(ln)
if ln.strip().startswith('#'):
out.append('<span class="c">%s</span>' % e) # コメント
elif ln.startswith('$'):
out.append('<span class="k">$</span>' + e[1:]) # プロンプト
else:
out.append(e)
return '<pre><code>%s</code></pre>' % ' '.join(out)
Markdown を Python で組み立てる
レールの章立て・ロゴ・進捗バーは全スライドに出ます。手で 18 回書くとズレるので、生成スクリプトを 1 枚はさみました。これが結果的にいちばん効きました。
SECTIONS = [
('01', 'Marp とは'),
('02', '環境をつくる'),
('03', 'テーマを自作する'),
('04', 'Claude Code への頼み方'),
('05', '出力して公開する'),
]
def rail(active=None, progress=0.0):
"""左レール(章立て)+ロゴ+進捗バーを組み立てる"""
items = ['<b>目次</b>']
for num, name in SECTIONS:
on = ' class="on"' if num == active else ''
items.append('<span%s><i>%s</i>%s</span>' % (on, num, name))
step = max(1, min(18, round(progress * 18)))
return ('<div class="toc">%s</div>\n'
'<div class="brand">Sil-port</div>\n'
'<div class="bar" -p%d></div>' % (''.join(items), step))
SLIDES = []
def S(body, cls=None, active=None, rail_on=True):
"""1枚ぶんを積む。cls でレイアウトの型を指定する"""
head = '<!-- _class: %s -->\n' % cls if cls else ''
idx = len(SLIDES) + 1
r = rail(active, idx / 18) if rail_on else ''
SLIDES.append(head + body.rstrip() + ('\n\n' + r if r else ''))
# 使うときはこう
S('# Markdown が、そのままスライドになる\n\n...', active='01')
S('<p class="num">02</p><h2>環境をつくる</h2>', cls='divider', rail_on=False)
MD = FRONT + '\n\n---\n\n'.join(SLIDES)
こうしておくと、Claude Code に頼むときも「7 枚目の右カラムをコードブロックに差し替えて」で通ります。レールやロゴの心配をしなくてよくなるのが大きい。
レイアウトは「型」で持つ
毎回ゼロから組ませず、使い回す型を CSS 側に用意しておきます。これがあると頼み方が短くなり、出てくるものが安定します。
| クラス | 用途 |
|---|---|
.cover | 表紙。余白と背景をまるごと差し替える |
.divider | 中扉。背景をアクセント色で塗る |
.cols | 2 カラム。-w -n で比率を変える |
.steps | 番号つきの手順。丸と縦線は CSS で描く |
.tiles | 3 列のカード。目次や一覧に使う |
.card | 補足の囲み。左に色の線を引く |
.lead | リード文。太字大きめ、キーワードだけアクセント色 |
Claude Code への頼み方
同じことを頼んでも、言い方で手直しの量が変わります。
| 頼み方 | |
|---|---|
| 効かない | 「Marp でかっこいいスライドを作って」 「かっこいい」の基準が共有されていない。枚数も対象読者も決まっていない。結果として汎用テンプレートが出てくる |
| 効く | 「このURLのスライドのデザインを真似して、Marp のテーマ CSS を書いて。16:9、左に章立ての固定レール、見出しは左に縦線。まず 3 枚だけ作って PDF にして見せて」 |
揃えるべきは 4 つです。
言葉で説明するより速い。実際に見に行って再現します。今回もメルカリのレポートの URL を渡しただけで、レールの構成まで拾ってきました。
比率・レール・見出しの扱い。ここを任せると毎回違うものが出てきます。「型」として CSS のクラス名まで決めてしまうのが確実です。
18 枚作らせてから直すより、3 枚で方向を決めるほうが速い。直しが全スライドに波及する前に方向を確定させます。
「PNG にして見せて」まで言うと、自分で確認して直します。ここを言わないと、Markdown を書いて終わりになります。
確認のループを回す
# ブラウザで見ながら書く(人間向け)
npx marp -s .
# 保存のたびに PDF を作り直す
npx marp slides.md --pdf --watch
# 1枚ずつ PNG にする(AI に確認させる用)
npx marp slides.md --images png --image-scale 1 -o out/s.png
18 枚を 6 枚ずつ 3 枚のコンタクトシートにまとめると、一度に全体を見渡せて効率が上がります。これも数行で作れます。
// out/*.png を 2 列のグリッドに並べて1枚の画像にする
const files = fs.readdirSync('out').filter(f => f.endsWith('.png')).sort();
const imgs = files.slice(0, 6).map(f =>
'data:image/png;base64,' + fs.readFileSync('out/' + f).toString('base64'));
const html = `<style>body{margin:0;display:grid;grid-template-columns:1fr 1fr;gap:8px}
img{width:100%;display:block}</style>`
+ imgs.map(s => `<img src="${s}">`).join('');
await page.setContent(html);
await page.screenshot({ path: 'grid.png', fullPage: true });
出力形式と、その使い分け
| 形式 | コマンド | 使う場面 |
|---|---|---|
--pdf | 配布・Speaker Deck への投稿 | |
| PNG | --images png | 1 枚ずつ確認・SNS・記事への埋め込み |
| HTML | (既定) | そのまま Web に置く。リンクが生きる |
| PPTX | --pptx | PowerPoint で渡す必要があるとき |
PDF にタイトルなどのメタデータを入れたいときは、フロントマターに書けば入ります。
---
marp: true
title: Claude Code で Marp スライドをつくる
description: Markdown だけでスライドを組み、テーマ CSS まで自作するまで。
author: Sil-port
---
Speaker Deck に載せて、記事に埋め込む
npx marp slides.md --pdf --browser chrome --no-stdin。今回は 18 枚で約 4.7MB になりました。
PDF を選んでタイトルと説明を入れるだけです。変換に数分かかります。
スライドのページで Share → Embed。speakerdeck.com/player/<ID> を指す iframe が取れます。
WordPress ならカスタム HTML ブロックにそのまま置きます。高さは padding-top:56.25% で 16:9 を維持するのが確実です。
<div class="sd-embed">
<iframe src="https://speakerdeck.com/player/<ID>"
title="スライドのタイトル" allowfullscreen loading="lazy"
allow="encrypted-media;fullscreen"
style="border:0;width:100%;height:100%;position:absolute;left:0;top:0"></iframe>
</div>
<style>
.sd-embed { position: relative; width: 100%; padding-top: 56.25%; }
</style>
向いている場面、向いていない場面
| 向いている | 向いていない |
|---|---|
| 文字と表とコードが中心の資料(技術資料・社内共有・勉強会) | 1 枚ごとに自由に配置したい資料。CSS で座標指定することになり、GUI のほうが速い |
| 同じ型を何度も使うもの(月次レポート・定例の報告) | アニメーションが要るもの。ページ送り以外の演出はほぼできない |
| 更新が続くもの。差分が読めるので改訂が楽 | 相手が編集する前提のもの。PPTX で出しても中身は画像 |
| AI に任せたいもの。テキストなので全部読ませられる |
Marp が速いのは、デザインを CSS に閉じ込めて、中身を Markdown に閉じ込めているから。この分離がそのまま「AI に任せられる範囲」と「自分で決める範囲」の境界になる。
結論として、型は自分で決めて、中身は任せる。それが一番手戻りの少ない使い方でした。今回のスライドも、テーマ CSS の構造を決めるところまでは自分で指示を出し、18 枚の中身とレイアウトの当てはめは全部任せています。
関連記事
よくある質問
- Marp とは何ですか?
Markdown をスライドに変換する仕組みです。「---」で区切った1ブロックが1スライドになり、HTML・PDF・PNG・PPTX に出力できます。見た目はテーマCSSで決めるため、中身とデザインが完全に分かれているのが特徴です。
- Claude Code で Marp スライドを作るメリットは何ですか?
スライドがテキストファイルなので、AIが中身を読んで、直して、出力して、結果をPNG画像として目で確認するところまで自分で回せます。PowerPointやKeynoteはバイナリなので、この確認のループが成立しません。
- Marp で PDF が出力できないときは?
ほとんどの場合、Chrome / Chromium が見つかっていないことが原因です。CHROME_PATH 環境変数でパスを渡してください。また marp-cli が標準入力を待って固まることがあるので、--no-stdin を付けるか < /dev/null を渡すと解決します。
- Marp のテーマは自作できますか?
できます。1行目に /* @theme 名前 */ と書いたCSSファイルを用意し、--theme-set でディレクトリごと読み込ませるだけです。section が1枚のスライドに対応します。
- テーマCSSで content プロパティが効かないのはなぜですか?
Marp はページ番号を section::after で描画するため、テーマ側の content 宣言を落とします。擬似要素で背景などを置きたい場合は、section 自体の background に指定してください。
- HTML の data-* 属性が消えるのはなぜですか?
Marp の HTML サニタイザが data-* 属性と一部の style 属性を削除するためです。数字などを表示したい場合は attr() ではなく、実際の要素として書いてください。class 属性は残るので、バリエーションはクラスで持たせるのが確実です。
- コードブロックの中の --- がスライド区切りになってしまいます
を物理的に1行で出力し、改行を の実体参照にすると解決します。マークダウンパーサが行頭の --- を区切りとして解釈するのを避けられます。
- 作ったスライドはどこに公開すればいいですか?
PDF を Speaker Deck にアップロードするのが手軽です。Share → Embed で iframe の埋め込みコードが取れるので、ブログのカスタムHTMLブロックに貼れば記事に埋め込めます。
本記事は執筆時点(2026年8月)の Marp CLI v4.5.0 / marp-core v4.4.0 での挙動にもとづきます。バージョンによって仕様が変わる可能性があります。掲載しているコードは自由に使っていただいて構いません。
コメント