見せてあげよう、Marp の真髄を

2025/01/03に公開
3

みなさんは Marp でスライド作成していますか?

Marp は Markdown による軽量な記述方式と、あらかじめ用意されたテーマにより、美しいスライドを簡単に作成することができます。

今回はそんな Marp をフルに活用すべく、私が考える Marp の結論構成を紹介したいと思います!

対象者

この記事の対象読者は以下の通りです。

  • ✅ Marp のスライドデザインを細かくカスタマイズしたい
  • ✅ Marp で HTML タグをいちいち書くのが億劫
  • ✅ Marp で作ったスライドを楽に Web で公開したい

逆に、下記のような方は対象外となっています。

  • ❌ Marp の概要/基本的な使い方が知りたい
  • ❌ Marp の最小限の機能/現状で満足している

また、詳しい理由は後述しますが、以下の方は部分的に対象外です。

  • ⚠️ Marp で作ったスライドは PDF で共有したい
  • ⚠️ Marp は VSCode の拡張機能だけで完結させたい

以上をご理解いただき、特に自分が対象だと思われた方はぜひ最後までご一読ください🙏

結論構成

まず先に結論を申し上げます。
今回のメインテーマはズバリ、「CSS ユーティリティクラスと Markdown 拡張構文の合わせ技」です!
他にも、スライドを Web で閲覧可能にするための手順や、便利なテクニックなども解説します。

なお、今回紹介する構成を簡単に始められるようテンプレートを用意しました。

https://github.com/yKicchan/awesome-marp-template

もし最後まで読んでみて真似したくなった方はぜひご活用いただければ嬉しいです。

CSS ユーティリティクラスを用意する

Marp を少しでも拡張しようと思われた方はご存知だと思いますが、Marp は Marpit の機能によって、CSS を使った独自のカスタムテーマを適応することができます。
(カスタムテーマを利用するための設定方法などは他にたくさんの解説記事があるためここでは割愛します)

カスタムテーマを用意すれば自分で CSS を記述できるため、CSS セレクタを利用した自在なスライドデザインが可能になります。
私がおすすめしたいのは ユーティリティクラスを用意するということです。

文字の大きさを変える

例えばスライド内でもう少し文字を大きく(小さく)したいというケースがあります。
このために文字の大きさを変更するユーティリティクラスを用意しておけば、それ以降は用意したクラスを利用するだけで簡単に解決することが可能です。

myTheme.css
.text-xl {
  font-size: 1.25em;
}
.text-lg {
  font-size: 1.125em;
}
.text-sm {
  font-size: .875em;
}
.text-xs {
  font-size: .75em;
}
slide.md
## テキストの大きさを調整する

<div class="text-lg">
ちょっと大きくしたい
</div>
<div class="text-sm">
ちょっと小さくしたい
</div>

色や余白感を変える

もちろん大きさだけでなく、余白の調整や文字色などの変更もユーティリティクラスを用意しておけば簡単に表現できます。

myTheme.css
.mt-1 {
  margin-top: 1em;
}
.blue {
  color: blue;
}
## テキストの余白感や色を調整する

<div class="blue">
青い文字色
</div>
<div class="mt-1">
上に少しの余白を確保
</div>

有名どころだと、列表示用の .col のようなユーティリティクラスの知見をよく見かけます。
このようにカスタムテーマの定義にユーティリティクラスを実装しておくことで、スライド毎に <style> をいちいち定義することなく手軽にスライドの表現力を高めることが可能です。

...でもいちいち <div class="hoge"></div> とか毎回書くのもめんどくさいし HTML タグが出てくるのイけてなくないですか?
せっかく Marp で爆速スライド作成するならもっとイけてる感じにしたいと思います。

Markdown の構文を拡張する

カスタムテーマに比べてこちらはあまりメジャーではなさそう(自分調べ)だったのですが、Marp は markdown-it のプラグインを利用することが可能です。

有名どころだと、PlantUML を使えるようにするとかですかね。
もちろんそういうのもいいのですが、私がおすすめしたいのは以下3つのプラグインの活用です。

npm パッケージとしてインストール後、導入設定は以下のようになります。

engine.mjs
import container from 'markdown-it-container';
import attrs from 'markdown-it-attrs';
import mark from 'markdown-it-mark'

/** 
 * @type {import('@marp-team/marp-cli').Config<typeof import('@marp-team/marpit').Marpit>["engine"]}
 */
export default ({ marp }) => marp
  .use(mark)
  .use(attrs)
  .use(container, 'name'); // ← 第二引数の 'name' については後述します。

この js ファイルを Marp CLI のオプションに渡すよう設定し、開発サーバーを起動すれば完了です。

.marprc.yml
engine: engine.mjs
package.json
"scripts": {
  "dev": "marp -w --html -s slides"
}
$ npm run dev

ではそれぞれの使い方を詳しく見ていきましょう

markdown-it-mark

== で文字を囲んで <mark> に変換してくれるプラグインです。

input
==マークされるよ==
output
<mark>マークされるよ</mark>

え?それだけ?ってなったかもですが、慌てずに。
本番はここからです。

markdown-it-attrs

{.class}{attr=value} とすることで、要素にクラスや属性を付与できます。
勘の良い方はもうこの威力に気づいたかもしれません。

input
クラスがつくよ{.hoge}

属性がつくよ{data-id=piyo}
output
<p class="hoge">クラスがつくよ</p>
<p data-id="piyo">属性がつくよ</p>

markdown-it-container

::: で囲うことで、あらかじめ定義した名前をクラスにしたコンテナ要素に変換できます。

あらかじめ定義した名前

というのは、markdown-it-container プラグインを導入する際に設定が必須な文字列です。
先ほどの導入設定時の 'name' がこれに当たります。
下記のように複数設定することも可能です。

engine.mjs
import container from 'markdown-it-container';

export default ({ marp }) => marp
  .use(container, 'name1')
  .use(container, 'name2');

では仮に :::hoge という名前を定義します。

engine.mjs
import container from 'markdown-it-container';

export default ({ marp }) => marp
  .use(container, 'hoge');

これを利用する場合は以下のようになります。

input
:::hoge
グルーピングされるよ

グルーピングされるよ

グルーピングされるよ
:::
output
<div class="hoge">
  <p>グルーピングされるよ</p>
  <p>グルーピングされるよ</p>
  <p>グルーピングされるよ</p>
</div>
ちなみに定義していない名前には反応しません。
input
:::foo
そのまま HTML になるよ
:::
output
<p>:::foo<br>そのまま HTML になるよ<br>:::</p>

まるでzenn独自記法ですね!(同じプラグインを利用しているかは知りません)

CSS ユーティリティクラスと Markdown 拡張構文の合わせ技

お待たせしました、やっと本題です。
CSS ユーティリティクラスをカスタムテーマに用意し、紹介した Markdown 拡張構文を利用して組み合わせるとどうなるかご紹介します🚀

簡単に文字のスタイルを変える

文字の大きさを変える で紹介した Markdown の入力は下記でした

before
<div class="text-lg">
ちょっと大きくしたい
</div>
<div class="text-sm">
ちょっと小さくしたい
</div>

拡張構文を利用すればこうなります!

after
ちょっと大きくしたい{.text-lg}
ちょっと小さくしたい{.text-sm}

markdown-it-attrs によりクラスの付与が簡易になったため、<div> を書かなくてもよくなりました!

簡単に特定の文字列だけスタイルを変える

スライドを作成していると、一文の中の特定の文字列のみ装飾したいケースも出てきます。
そのときに威力を発揮するのが markdown-it-markmarkdown-it-attrs の合体技です!

input
この文章の中の==この文字=={.red}だけ赤色にする
output
<p>この文章の中の<mark class="red">この文字</mark>だけ赤色にする</p>

== により <mark> インライン要素に変換しつつ、{.red} で文字色を赤にするユーティリティクラスを <mark> に付与しています。
すごく地味ですが、スライド作成においてかなり痒いところに効いてくるテクニックでおすすめです!

コードブロックにファイル名を表示する

markdown-it-attrs により、コードブロックにも直接クラスや属性を付与できるようになりました。
これを利用して、特定の属性がコードブロックに付与されている場合、CSSでその値を使って表示することでファイル名表示を実装することができます!

以下は gaia テーマでコードブロックに data-name 属性を利用してファイル名を表示する実装例です。

myTheme.css
code[data-name] {
  position: relative;
  margin-top: 44px;
  padding-top: 4px;
}
code[data-name]::before {
  position: absolute;
  top: -44px;
  left: 0;
  padding: .25em .75em;
  border-top-left-radius: 8px;
  border-bottom-right-radius: 8px;
  content: attr(data-name);
  font-size: .75em;
  background: var(--color-dimmed);
}
slide.md
```tsx {data-name=component.tsx}
interface P {
  value: string;
  onSubmit: (v: string) => void;
}

export const Component: FC<P> = ({ value, onSubmit }) => (
  <button type="button" onClick={() => onSubmit(value)}>
    {value}
  </button>
);
```

表示は以下のようになります。

コードブロックにファイル名を表示しているスライド画像

一括でスタイルを変更する

今度は markdown-it-container が主役で、一部 markdown-it-attrs を組み合わせて使います。

ではまず markdown-it-container のプラグイン導入設定で col という名前を登録します。

engine.js
import container from 'markdown-it-container';

export default ({ marp }) => marp
  .use(container, 'col');

次に .col という列表示のためのユーティリティクラスをカスタムテーマに定義します。

myTheme.css
.col {
  display: flex;
  gap: 1rem;
}
.col > * {
  flex: 1;
}

最後に :::col を使って列表示のスライドを作成します!

slide.md
:::col
左に表示されるよ

中に表示されるよ

右に表示されるよ
:::
output
<div class="col">
  <p>左に表示されるよ</p>
  <p>中に表示されるよ</p>
  <p>右に表示されるよ</p>
</div>

flex-container である .col クラスがそれぞれの <p> を囲った <div> に付与されていることで、簡単に列表示に対応することができました!

一括でスタイルを変更する応用

次はいろんなスタイルを一括で指定する方法をご紹介します。

markdown-it-container は先ほど「プラグイン導入設定で名前が必須」と解説しました。
この必須というところがネックで、文字サイズや色などの多岐にわたるユーティリティクラスを、全て対応させるのは現実的ではありません。

仮に変更するとしても Marp CLIWatch mode を利用して、ブラウザ上でスライドを確認している状態では定義の変更ごとに設定の再読み込みが必要で、開発サーバーを再起動する手間も生まれてしまいます。

そこで私がおすすめするのが、よく使うものに加えてダミークラスを定義しておくということです。

engine.js
 import container from 'markdown-it-container';

 export default ({ marp }) => marp
   .use(container, 'col')
+  .use(container, '_');

今回は _ をダミークラスとしています。
これはカスタムテーマの中で定義していないので、何もスタイルの影響は生まれません。
意味的には <div> で囲われるのみになります。
ではこれをどう活用するのか紹介します。

列表示時などのグルーピング

まずはただ <div> で囲いたい場面での活用方法です。
下記のように、複数の要素をそれぞれ右と左に表示したい場合に利用できます。

slide.md
::::col
:::_
## 左のタイトル
左の文字
:::
:::_
## 右のタイトル
右の文字
:::
::::

ちょっと : が多いので、<div> をそのまま使う派と好みが分かれそうですね!

ユーティリティクラスの一括付与

markdown-it-containermarkdown-it-attrs を組み合わせてクラスを付与することが可能です!

slide.md
:::_ {.blue}
これは青い文字

これも青い文字

ぜんぶ青い文字
:::

markdown-it-attrs を利用することで、プラグイン設定を変更することなく一括で全てのユーティリティクラスを付与できるようになってしまいました...!

私はこの使い方を気に入っており、もう Marp のスライドに HTML タグが出てくることがほぼなくなってしまいました。

SyntaxHighlight のテーマを変更する

Marp の提供するデフォルトの SyntaxHighlight で十分かもしれませんが、私のように宗教上の理由などで自分が好む色合いに変更したい場合があります。

Marp は highlightjs を利用しているので、highlightjs が提供する CSS から好きなものをコピペしてカスタムテーマに含めれば簡単にスタイルを変更することができます!

しかし、私は JetBrains darcula 教の信徒です。
残念ながら highlightjs の豊富なテーマには darcula が含まれていません。
別の SyntaxHighlighter である Prism.js にはあるのに...

そのような方のために Prism.js に簡単に対応する方法をご紹介します!

markdown-it-prism を導入する

markdown-it のプラグインの一つに、Prism.js に対応してくれるものがあります。
これを利用して Prism.js 仕様に変更しましょう!

engine.js
import prism from 'markdown-it-prism';

export default ({ marp }) => marp
  .use(prism);

プラグインを設定したらあとは CSS を読み込むだけです。
なお、読み込めるテーマの一覧は prism-themes の中から選べます。
こちらは CDN でも配布されているのでそこからインポートします。

myTheme.css
@import url('https://cdnjs.cloudflare.com/ajax/libs/prism-themes/1.9.0/prism-darcula.min.css');

なんとこれで SyntaxHighlight の配色を変更することができました!

darcula テーマによる SyntaxHighlight のコード

GitHubPages にデプロイする

ここまででスライドをカスタムしてきましたが、最後に Web で公開するまでの手順をご紹介します。
利用するのは GitHubPages で、Web(GitHubPages) に公開する理由は以下の通りです。

  • リポジトリがあればそのまま使えるため、自分でホスティング先を用意する必要がない
  • Web なので URL による共有が簡単にできる
  • (SpeakerDeckと違って)閲覧時にハイパーリンクが機能する
  • 自動化により公開の手間をなくせる(URL共有のみにできる)

デメリットは PDF と違って端末差異が発生しうることですね。

ここでは GitHubActions を使った具体的なデプロイ設定や、OGP 画像の表示方法を解説します。

GitHubActions のデプロイ設定

ビルド(スライドのHTML化)簡略化のため、まずはスクリプトを用意しておきます。
また、スライドの実態は slides ディレクトリ内にあるとここでは仮定します。

package.json
 "scripts": {
   "dev": "marp -w --html -s slides",
+  "prebuild": "rm -rf dist && mkdir -p dist && cp -r slides/* dist",
+  "build": "marp dist"
 }

この例では、ローカルでも簡単に動作検証できるよう dist フォルダが成果物となるようにしています。
.gitignore で git の管理から外しておくと都合がいいです。

次に GitHubActions のワークフローを作成します。

.github/workflows/deploy.yml
name: Deploy

on:
  workflow_dispatch:
  push:
    branches:
      - main
    paths:
      - "slides/**"
      - ".github/workflows/deploy.yml"

env:
  TZ: 'Asia/Tokyo'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          cache: "npm"
      - name: Install dependencies
        run: npm ci
      - name: Build slides
        run: npm run build
      - name: Upload artifacts
        uses: actions/upload-pages-artifact@v3
        with:
          path: "dist/"
  deploy:
    needs: build
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

あとは カスタム GitHub Actions ワークフローによる公開 を参考にリポジトリの設定を変更しておけば、GitHubActions から GitHubPages に自動的にデプロイされるようになりました!🚀

タイトルスライドを OGP 画像に設定する

先の設定によりスライドが Web で閲覧可能になりましたが、URL を共有しても OGP 画像が表示されません。
タイトルスライドを OGP 画像にするにはいくつかの設定が必要です。

スライドのメタデータに OGP 画像パスを設定する

スライドの先頭に記述するメタデータに、OGP 画像用の設定を記述できます。
metadataimage global directive を利用します。
下記は GitHubPages に OGP 画像を用意する前提のURL例です。

slide.md
---
marp: true
image: https://<username>.github.io/<repo>/path/to/slide.png
---

タイトルスライドを画像化する

Marp CLI の image オプション はタイトルスライドを指定の画像形式で変換してくれます。
これを利用してタイトルスライドを画像化するよう先ほど用意したビルドコマンドを変更します。

package.json
 "scripts": {
   "dev": "marp -w --html -s slides",
   "prebuild": "rm -rf dist && mkdir -p dist && cp -r slides/* dist",
-  "build": "marp dist"
+  "build": "marp dist --image png && marp dist"
 }

デプロイ時にフォントをインストールする

タイトルスライドを画像化する場合、日本語などマルチバイトの文字列は文字化けしてしまいます。
これはランナーにマルチバイトに対応したフォントが存在しないが原因なので、適当なフォントをインストールして回避します。

.github/workflows/deploy.yml
 jobs:
   build:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
         uses: actions/checkout@v4
       - name: Use Node.js
         uses: actions/setup-node@v4
         with:
           cache: "npm"
       - name: Install dependencies
         run: npm ci
+      - name: Install font
+        run: sudo apt install fonts-noto
       - name: Build slides
         run: npm run build
       - name: Upload artifacts
         uses: actions/upload-pages-artifact@v3
         with:
           path: "dist/"

以上でタイトルスライドの OGP 画像設定は完了です!

...え?、OPG 画像のURLをわざわざ書くのが面倒だって?
私もそう思います。

タイトルスライドをほぼ完全自動で OGP 画像に設定する

先で紹介した通り、あらかじめ OGP 画像へのURL(相対パスは不可)を記述する必要があります。
なのでスライドを管理するディレクトリには一定のルールを設けることをおすすめします。
具体的には、下記のようにします。

slides/    # ← スライドを格納するルートディレクトリ、ここの名前はなんでもいい
|
-- slide1/  # ← スライドの名前を表すディレクトリ、この階層からURLになる
|    |-- images/   # ← スライドで利用する画像を格納するディレクトリ
|     -- index.md  # ← スライドのマークダウン。 index.html と index.png になるのでこの名前を固定する
|
-- group/
     |
     -- slide2/ # ← 必要に応じてグルーピングしてもいいが、この中のスライドは index.md に名前を固定しておく

このルールにより、OGP 画像のパスが機械的に特定できるようになりました!
ということは自動化できるということです。

テンプレートスライドを用意する

OGP 関係なくテンプレートスライドを用意しておくことに越したことはありませんが、いい活用方法があるのでご紹介します。

テンプレートスライドを先ほどの命名ルールに従って、下記のように用意します。

template/index.md
---
marp: true
image: https://<username>.github.io/<repo>/{{PATH}}/index.png
他の設定は割愛
---

<username><repo> には実際の値を入れてください。
{{PATH}} はそのままでOKです。

テンプレートから新規スライドを作成するコマンドを用意する

次にテンプレートスライドから新しいスライドを新規作成するコマンドを用意します。

scripts/new
#!/bin/bash

mkdir -p src/$1
cp -r template/* src/$1

sed -i '' "s|{{PATH}}|$1|" src/$1/index.md

最後にこのコマンドを簡単に利用できるよう npm scripts に登録しておきましょう

package.json
   "dev": "marp -w --html -s slides",
   "prebuild": "rm -rf dist && mkdir -p dist && cp -r slides/* dist",
   "build": "marp dist --image png && marp dist",
+  "new": "scripts/new"

こうすることでスライドの名前を受け取りながら、その名前に応じてテンプレートからスライドが生成され、さらに OGP 画像のパスが自動で書き換わるようになりました!

shell
$ npm run new topic
$ npm run new group/topic
topic/index.md
---
marp: true
image: https://<username>.github.io/<repo>/topic/index.png
---
group/topic/index.md
---
marp: true
image: https://<username>.github.io/<repo>/group/topic/index.png
---

これでだいぶ楽になりましたが、まだスライドのディレクトリ名を変更するなどで簡単に壊れてしまいます。
名前変更用のスクリプトを組んだとしても、人は愚かなので使うのを忘れます。

こういうのはデプロイ時に CI でチェックするようにするのがおすすめです!

OGP の URL を CI でチェックする

以下はスライドメタデータの OGP 画像の設定が、ディレクトリ構造とマッチしているか簡易的に検証するスクリプトの例です。
ローカル実行時には自動でURLを修正する機構も組み込み込んでいます。

https://github.com/yKicchan/awesome-marp-template/blob/main/scripts/check

このスクリプトを deploy job に組み込み、チェックが通らなければデプロイがコケるようにします

.github/workflows/deploy.yml
 jobs:
   build:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
         uses: actions/checkout@v4
+      - name: Check OGP image url
+      - run: ./scripts/check
       - name: Use Node.js
         uses: actions/setup-node@v4
         with:
           cache: "npm"
       - name: Install dependencies
         run: npm ci
       - name: Install font
         run: sudo apt install fonts-noto
       - name: Build slides
         run: npm run build
       - name: Upload artifacts
         uses: actions/upload-pages-artifact@v3
         with:
           path: "dist/"

これにより OGP の設定ミスに気づけるようになりました!🎉
また万が一URLがおかしくなっても、ローカルでこのスクリプトを実行し自動で修正できるため、コミットするだけで良くなります👏

目的のスライドの URL を取得しやすくする

GitHubPages は、存在しない URL にアクセスした時 404 を返します。
これだけ聞くと当然かのようですが、ローカルや Apache サーバーだと下記画像のようなディレクトリ内を探索できるブラウジング環境を提供してくれます。

ファイル一覧がブラウザで表示されている機能の画像

この機能がないせいで目的のスライドの URL は手打ちで入手する必要があり、少し手間です。
また、ルートページが同じ理由で 404 になってしまうのもイけてませんね。
こういった事象に困っている人はたまに観測しており、大抵諦めてURLを手打ちしているか、ルートページに手書きのページ一覧を用意しているかが大半のようでした。(自分調べ)

なのでそれを解消するための GitHubAction を作成しました。
詳しくは下記記事を参照してください。
https://zenn.dev/ykicchan/articles/f42b708fa332a4

まとめ

かなり長くなってしまいましたが、果たしてここまで読み進めた猛者はいるのでしょうか...?
今回は Marp の最強活用術をご紹介しました。

  • カスタムテーマでユーティリティクラスを作成すること
  • markdown-it のプラグインで Markdown の構文を拡張すること
  • ユーティリティクラスと Markdown 拡張構文を合わせて表現力を上げること
  • GitHubPages にデプロイして共有を楽にすること

お忘れかもしれませんが、今回紹介した内容をすぐに利用できるテンプレートリポジトリを用意しています。

https://github.com/yKicchan/awesome-marp-template

もし「真似してみたい!」と思っていただけた方は、テンプレートリポジトリから作成いただくか、内容を参考にしてコピペなどしてもらえると喜びます👍

それではよい Marp ライフを〜👋

Discussion

きっちゃそきっちゃそ
  1. OGP の URL を CI でチェックする の中のチェックスクリプトを、テンプレートリポジトリの実態を参照するように変更しました。
  2. また、同スクリプトをローカルで実行した時に、間違った URL を自動修正できるようにアップデートしました。
LaPhLaPh

https://github.com/zenn-dev/zenn-editor/blob/canary/packages%2Fzenn-markdown-html%2Fpackage.json#L55-L55

zenn-markdown-htmlのpackage.json見ると分かりますが、markdownit-container使用しているので、同じプラグインだと思います。

きっちゃそきっちゃそ

閲覧&コメントありがとうございます!

おー確かに同じっぽいですね!
Marpみたいな自分が入力する分には記述量が減るくらいの恩恵ですが、Zennのような第三者が自由に入力できるところは、セキュリティの観点でこういった拡張構文によるアプローチが必要ですね👍