Engineer‘s Blogkoukibuu3
「ブログにスライド閲覧機能を追加する:Slidev の活用法」のメイン画像

ブログにスライド閲覧機能を追加する:Slidev の活用法

  • JavaScript
  • pnpm
  • Slidev

Markdown でスライドが作れる Slidev を使って、ブログにスライド閲覧機能を実装します。

背景

スライドを Web 公開するサービスには以下がありますが、どうにも私は UI が好きではなくて…

特にスマホで閲覧する際、ナビゲーションボタンが大きすぎてスライドの内容が見づらいと感じていました。

Docswell は他の 2 つよりシンプルな UI で、日本人の開発者によるサービスということもあり、個人的には応援したいサービスです。

Slidev の紹介

Slidev は、Markdown で記述した内容をスライドとして表示できるツールです。Qiita や VSCode の拡張機能、Obsidian でも同様の機能がありますが、 Slidev の最大の強みは 拡張性と汎用性の高さ です。

  • カスタマイズ性が高い:テンプレートを用意すれば、統一感のあるデザインのスライドを作成可能
  • Vue.js コンポーネントを埋め込める:独自のインタラクティブ要素を追加できる
  • 発表者モードや Picture-in-Picture などの機能:発表時に便利な機能が充実

ローカル環境で npm run dev を実行すればスライドを確認でき、ビルドすれば SPA としてホスティングも可能です。

今回は 公式ドキュメント を参考に、Slidev をブログに組み込んでみます。

使用要素

今回使用する主な技術スタックは以下のとおりです。

  • Slidev
  • Vue.js
  • pnpm workspace
  • Markdown / YAML Front Matter
  • GitHub Pages
  • GitHub Actions

ディレクトリ構成

├── list.mjs
├── package.json
├── packages
│   ├── 221124-name-development
│   │   ├── index.html
│   │   ├── node_modules
│   │   │   └── slidev-theme-one -> ../../theme
│   │   ├── package.json
│   │   ├── pages
│   │   │   └── title.md // タイトルスライド
│   │   ├── public
│   │   │   └── {画像ファイル}
│   │   └── slides.md // スライド本体
│   ├── 221208-extends-disadvantage
│   ├── {YYMMDD}-{title}
│   └── theme
│       ├── README.md
│       ├── layouts
│       │   ├── cover.vue
│       │   ├── default.vue
│       │   ├── fact.vue
│       │   ├── intro.vue
│       │   └── splash.vue
│       ├── package.json
│       └── styles
│           ├── index.ts
│           └── layout.css
├── pnpm-lock.yaml
└── pnpm-workspace.yaml

発生した課題とその解決策

Slidev はデフォルトで 1 スライド=1 npm package という構成になっているため、通常はスライドごとに別々のリポジトリを作成する必要があります。複数スライドを作ろうと思ったらその都度リポジトリが増えるわけですね。それは嫌だ…

この問題に対して、以下の 2 つの方法を検討しました。

1. 1 つのスライドで頑張る(不採用)

複数スライドを 1 つにまとめて、リンクでジャンプできるようにする方法です。しかし、スライドが増えるほど管理が大変になるため、不採用としました。

2. モノレポ構成にする(採用)

pnpm workspace を使用し、複数のスライドを 1 つのリポジトリで管理する方法を採用しました。以下の記事を参考にしています。先駆者が既に開拓してくれていました。ありがたい。

資料に加えて、今回調整したポイントを少しだけ紹介します。

packages:
- "packages/*"

packages/ ディレクトリ配下の任意のディレクトリをワークスペースで管理します。

{
  "scripts": {
    "build": "pnpm run -r build",
  }
}

ルートの package.json のビルドコマンドでは、ワークスペースで管理している個別パッケージの build コマンドを並列実行します。

{
  "scripts": {
    "dev": "slidev --open",
    "build": "slidev build --out ../../dist/221124-name-development --base /slides/221124-name-development"
  }
}

個別パッケージでは build コマンドにおいて出力先ディレクトリを、個別パッケージの外の /dist に設定しています。また、base を /slides/221124-name-development に設定することで、パス解決が失敗しないようにします。

ディレクトリ名を直接指定するのはイケてないですね。ここを自分で書くのは大変なので、template 作成用のスクリプトを用意するか、勝手に取得するように改善したいです。

dev は開発用のコマンドです。個別パッケージのディレクトリ配下で実行して表示確認をします。

調整したのはこのくらいですが、モノレポ構成にしたことで後述するいくつかの問題が発生しました。ここからはその解消をしていきます。

課題②:画像のパスがずれる

Slidev は base 設定を使ってルートパスを変更できますが、YAML Front Matter の image パスには適用されません。そのため、不格好ですが sed コマンドを使ってビルド前に md ファイルを直接書き換えました。

{
  "scripts": {
    "fix:image": "find ./packages -name '*.md' | xargs sed -i 's/image: \"\\/public/image: \"\\./g'"
  }
}

※ MacOS では sed -i '' のようにオプションを調整する必要があります。

課題③:サムネイル用の画像が上手く生成できない

サムネイル用に title.md だけ PNG エクスポートさせて、その画像を利用したかったのですが、なぜかタイトルページ単体のエクスポートでは画像が適用されませんでした。

そのため、1 ページ目以外は無駄なのですが全ページを PNG 出力することにしています。

export を実行するには Playwright が必要なので、ローカルで実行する際は必要に応じて pnpm exec playwright install などでインストールします。

ちなみに、png や pdf のエクスポートのコマンドは提供されていますが、dev コマンドで立ち上げた後に画面上の機能から生成する方法が推奨されているようです。今回は CI で自動生成させたいのでコマンドを利用します。

課題④:ブログサイトから動的に参照させたい

ブログサイトからスライドを動的に取得するため、以下の 3 つを用意しました。

  1. GitHub Pages をホスティングとして利用
  2. GitHub Actions でスライドを自動デプロイ
  3. スライドの一覧を JSON で提供する疑似 API

以下ワークフローの設定内容です。

name: Deploy pages
on: push
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '18'
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Cache pnpm modules
        uses: actions/cache@v4
        with:
          path: ~/.pnpm-store
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-
      - name: Install fonts
        run: |
          sudo apt-get install fonts-noto-cjk
      - name: Setting locale
        run: |
          sudo locale-gen ja_JP.UTF-8
          sudo update-locale LANG=ja_JP.UTF-8
      - name: Install dependencies
        run: pnpm install
      - name: Modify path to public
        run: pnpm run fix:image
      - name: Build all packages
        run: pnpm run -r build
      - name: Install Playwright browsers
        run: pnpm exec playwright install chromium
      - name: Export PNG for all packages
        run: pnpm --workspace-concurrency=1 run -r export:png
      - name: Generate index page
        run: pnpm run generate:index
      - name: Deploy pages to GitHub Pages
        uses: crazy-max/ghaction-github-pages@v2
        with:
          build_dir: dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Export PNG for all packages のステップでは、並列実行させるとエラーが発生しました。確かな原因は分からなかったので、ひとまず --workspace-concurrency=1 で同時実行数を 1 に指定することで回避しています。

API サーバーですが、このためにサーバーを建てたくはなかったので、GitHub Pages を擬似 API サーバーとして利用する方法を採用しました。

ビルドのタイミングでしかデータは動かないので、簡単な JS を書きスライドの一覧を JSON 形式で index.html として出力します。あとは取得側で 'Content-Type': 'application/json' として解釈してデータを取得すれば、ものぐさ API サーバーの完成です。

import fs from 'fs';
import path from 'path';

const baseUrl = 'https://koukibuu3.github.io/slides';

function getDirectoryContents(dirPath) {
  const contents = fs.readdirSync(dirPath, { withFileTypes: true });
  return contents.map(dirent => {
    if (!dirent.isDirectory()) return null;

    const indexHtml = fs.readFileSync(path.join(dirPath, dirent.name, 'index.html'), 'utf8');
    const title = indexHtml.match(/<title>(.*) - Slidev<\/title>/)?.[1] || dirent.name;

    return {
      title,
      url: `${baseUrl}/${dirent.name}`,
      mainImagePath: `${baseUrl}/${dirent.name}/images/1.png`
    };
  });
}

function main() {
  const directoryContents = getDirectoryContents('./dist');
  const jsonOutput = JSON.stringify(directoryContents, null, 2);
  console.log(jsonOutput);
  fs.writeFileSync('./dist/index.html', jsonOutput);
}

main();

まとめ

Slidev を使い、ブログにスライド閲覧機能を組み込む方法を紹介しました。

  • Markdown でスライドを作成
  • モノレポ構成で管理しやすくする
  • GitHub Pages でホスティングし、GitHub Actions で自動デプロイ
  • JSON を使った擬似 API でスライド一覧を提供

この方法なら、シンプルかつ管理しやすい形でスライドを公開できます。実際に動作している例は こちら 、リポジトリは GitHub に公開しています。