Claude Code で Marp スライドをつくる — テーマ自作から Speaker Deck 公開まで

読了時間の目安約 21 分10,082 字

つくる / Marp × Claude Code

スライドを Markdown で書くと、AI に丸ごと任せられるようになります。

テーマ CSS の自作から Speaker Deck への公開まで。実際に 18 枚のスライドを作った手順を、コードを全部載せて記録します。

結論

スライドを Marp(Markdown をスライドにする仕組み)で書くと、中身もデザインもテキストファイルになります。そのため Claude Code が読んで、直して、出力して、結果を目で確かめるまでを自分で回せるようになります。

PowerPoint や Keynote はバイナリなので、この最後の 2 つができません。そこが決定的な差です。

下に埋め込んだスライドは、この記事で説明する手順でつくったものです。テーマ CSS も生成スクリプトも全文を載せているので、そのままコピーして使えます。

目次

できあがったもの

スライド1枚目:表紙スライド2枚目:目次スライド3枚目:中扉 01スライド4枚目:Markdownがそのままスライドになるスライド5枚目:なぜClaude Codeと相性がいいのかスライド6枚目:中扉 02スライド7枚目:3つのコマンドで終わるスライド8枚目:最初の1枚スライド9枚目:中扉 03スライド10枚目:テーマはCSS1枚スライド11枚目:レイアウトは「型」で持つスライド12枚目:中扉 04スライド13枚目:効く頼み方、効かない頼み方スライド14枚目:直す→出す→見る、を回すスライド15枚目:中扉 05スライド16枚目:出力形式と、その使い分けスライド17枚目:向いている場面、向いていない場面スライド18枚目:おわりに
1 / 18

表紙

デザインは メルカリのサステナビリティレポートを参照しました。左に章立ての固定レール、見出しの左に縦線、下に進捗バー、というレポート型の構成です。「この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 / KeynoteMarp
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
もうひとつ、実際にいちばん時間を溶かしたのがこれです。marp-cli は標準入力を読もうとして待ち続けることがあります。ヒアドキュメントやスクリプト経由で叩くと数分固まるので、--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::aftercontent: "" + 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;
}
SVG を data URI にするときは base64 にしてください。URL エンコードのままだと、SVG 内の #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 行で出力し、改行を &#10; にすることです。

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>' % '&#10;'.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中扉。背景をアクセント色で塗る
.cols2 カラム。-w -n で比率を変える
.steps番号つきの手順。丸と縦線は CSS で描く
.tiles3 列のカード。目次や一覧に使う
.card補足の囲み。左に色の線を引く
.leadリード文。太字大きめ、キーワードだけアクセント色
型を先に決めておくと、あとは中身だけ頼めばよくなる

Claude Code への頼み方

同じことを頼んでも、言い方で手直しの量が変わります。

頼み方
効かない「Marp でかっこいいスライドを作って」
「かっこいい」の基準が共有されていない。枚数も対象読者も決まっていない。結果として汎用テンプレートが出てくる
効くこのURLのスライドのデザインを真似して、Marp のテーマ CSS を書いて。16:9、左に章立ての固定レール、見出しは左に縦線。まず 3 枚だけ作って PDF にして見せて

揃えるべきは 4 つです。

STEP
参照するデザインを URL で渡す

言葉で説明するより速い。実際に見に行って再現します。今回もメルカリのレポートの URL を渡しただけで、レールの構成まで拾ってきました。

STEP
構造を先に決める

比率・レール・見出しの扱い。ここを任せると毎回違うものが出てきます。「型」として CSS のクラス名まで決めてしまうのが確実です。

STEP
小さく出させて確認する

18 枚作らせてから直すより、3 枚で方向を決めるほうが速い。直しが全スライドに波及する前に方向を確定させます。

STEP
出力まで含めて頼む

「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
PNG で出すのがコツです。PDF だと全体しか見られませんが、PNG なら 1 枚ずつ画像として確認できます。「4 枚目の余白が詰まっている」「表が枠からはみ出している」まで気づけるようになります。

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--pdf配布・Speaker Deck への投稿
PNG--images png1 枚ずつ確認・SNS・記事への埋め込み
HTML(既定)そのまま Web に置く。リンクが生きる
PPTX--pptxPowerPoint で渡す必要があるとき
PPTX は各スライドが画像になるので、先方が編集する用途には向かない

PDF にタイトルなどのメタデータを入れたいときは、フロントマターに書けば入ります。

---
marp: true
title: Claude Code で Marp スライドをつくる
description: Markdown だけでスライドを組み、テーマ CSS まで自作するまで。
author: Sil-port
---

Speaker Deck に載せて、記事に埋め込む

STEP
PDF を書き出す

npx marp slides.md --pdf --browser chrome --no-stdin。今回は 18 枚で約 4.7MB になりました。

STEP
speakerdeck.com/new にアップロードする

PDF を選んでタイトルと説明を入れるだけです。変換に数分かかります。

STEP
埋め込みコードを取る

スライドのページで Share → Embed。speakerdeck.com/player/<ID> を指す iframe が取れます。

STEP
ブログに貼る

WordPress ならカスタム HTML ブロックにそのまま置きます。高さは padding-top:56.25% で 16:9 を維持するのが確実です。

<div class="sd-embed">
  <iframe src="https://speakerdeck.com/player/&lt;ID&gt;"
          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 での挙動にもとづきます。バージョンによって仕様が変わる可能性があります。掲載しているコードは自由に使っていただいて構いません。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次