本気の登壇をSlidevでやってみて感じた「のびしろ」

本気の登壇をSlidevでやってみて感じた「のびしろ」

この記事は エンペイ Advent Calendar 2023 3日目 の投稿です!昨日は @sanfrecce_osaka さんの「エンペイ Slack チャンネル探訪」でした!

エンジニアの ikuma-t です!今年は弊社が開催に携わったイベント含め、色々と登壇をさせていただきました。

登壇のスライドのいくつかは、MarkdownとVueでスライドを作成することのできるツール「Slidev」を使って作成しています。

いままでも身内LTくらいであればSlidevを使っていたのですが、外部向けの登壇ではFigmaを使ってスライドを作ることが多かったため、社外イベントでの採用は初めてでした。

この記事では今年の登壇を踏まえて、実際にSlidevをどのように利用しているか、使ってみてわかったのびしろをお伝えできればと思います!

Slidevとは

Slidevは開発者向けのスライド作成ツールです。以下のようにMarkdownで記述することでスライドを作成することができます。

Markdownを書くとスライドに変わる!!すごいぞ!
Markdownを書くとスライドに変わる!!すごいぞ!

VueやNuxt、Viteのコアチームである antfu 氏が開発されていることもあり、MarkdownだけではなくVue.jsやVueUse、UnoCSSなど、フロントエンド開発と同じような感覚でスライドを作成することができます。

Figmaではなく、Slidevで資料を作ろうと思ったわけ

スライド作成ツールに向き合っている時間にコードにさわれないことが苦痛に感じてきたためです。

昔はスライドデザインを考えてそれをFigmaに起こすだけでも楽しかったのですが、職業エンジニアになって1年が経ち、とうとうコードを書かない時間が苦痛になってしまいました。

これまでは自身の実装力不足や登壇経験不足に起因して、レイアウト調整のためのHTMLやCSSの記述に時間を回しながら期日までにスライドを作成するのが難しいと感じ、Slidevでの資料作成を断念していました。

しかし今の自分であればゴリゴリHTMLを書いてもまあなんとかなるだろうと思えてきたため、スライドもコードを触る時間とするべく、身内イベント以外の本気の登壇でもSlidevを使用することにしました。

本気の登壇を支える技術

ここからは実際にどうやってSlidevを使ったスライド作成を行っているのか、またスライド作成の際に便利だったTipsを紹介します。

Slidev用リポジトリの運用

Slidevには複数のスライドを生成する仕組みがありません。

最初はスライドごとにリポジトリを作成していましたが、SPAをデプロイする際やアセットの再利用で不便が多かったので、npm workspacesを用いて1つのリポジトリとしてまとめることにしました。

Slidev用リポジトリの構成。awesome-slideのディレクトリが登壇の数だけ増えます。
Slidev用リポジトリの構成。awesome-slideのディレクトリが登壇の数だけ増えます。

リポジトリルートでslidevのnpmをinstallして、各スライドディレクトリ内ではnpm scripts(e.g. "dev": "slidev" )だけを用意しています。

実際にはsrcディレクトリにはslidevに必要な設定ファイルや使用するコンポーネントが内包されているため、scaffdogを用いて共通部分は自動生成させています。

デプロイパイプライン

Slidevリポジトリのデプロイパイプライン
Slidevリポジトリのデプロイパイプライン

変更をGitHubにPushすることでCloudflare Pagesのデプロイが実行され、各スライドディレクトリのbuildコマンドにより生成されたSPAとPDFが公開される仕組みです。

ビルドコマンドを実行する際、Cloudflare Pagesのリダイレクト設定を動的に出力するスクリプト(scripts/redirect.ts)を実行することで、パスにごとにデプロイしています。

例えばKaigi on Rails 2023の資料であれば

となるようにデプロイされます。

参考:scripts/redirect.ts の詳細

動作はするものの、ベタベタに書いているので無駄があるかもしれません。

import fg from "fast-glob";
import fs from "node:fs/promises";
import { dirname, resolve } from "node:path";

const packageFiles = (
  await fg(["*/src/package.json", "!node_modules/**/*"], {
    onlyFiles: true,
  })
).sort();

const bases = (
  await Promise.all(
    packageFiles.map(async (file) => {
      const talkRoot = dirname(dirname(file));
      const json = JSON.parse(await fs.readFile(file, "utf-8"));
      const pdfFile = (
        await fg("*.pdf", {
          cwd: resolve(process.cwd(), talkRoot),
          onlyFiles: true,
        })
      )[0];
      const command = json.scripts?.build;
      if (!command) return;
      const base = command.match(/ --base (.*?)\s/)?.[1];
      if (!base) return;
      return {
        dir: talkRoot,
        base,
        pdfFile,
      };
    })
  )
).filter(Boolean);

const githubMainBranchUrl = "https://github.com/IkumaTadokoro/talks/blob/main";

const redirects = bases
  .flatMap(({ dir, base, pdfFile }) => {
    const parts = [];

    if (pdfFile) {
      parts.push(
        `${base}pdf ${githubMainBranchUrl}/${dir}/${pdfFile}?raw=true 302`,
        `/${dir}/pdf ${githubMainBranchUrl}/${dir}/${pdfFile}?raw=true 302`
      );
    }

    parts.push(
      `${base}src ${githubMainBranchUrl}/${dir} 302`,
      `${dir} https://talks.ikuma-t.com${base} 301`,
      `${base}* ${base}index.html 200`
    );

    return parts;
  })
  .join("\n");

const contents = `/ https://ikuma-t.com/talks 302
${redirects}
`;

await fs.writeFile("dist/_redirects", contents, "utf-8");

Cloudflare Accessによる限定公開プレビュー

公開先のCloudflare Pagesでは、Cloudflare Zero Trustに含まれるCloudflare Accessを利用することで、サイト閲覧に制限を設けることができます。

1つ目のSPAのパスは無制限。2つ目はメールアドレスで認証をかけているため、Cloudflare Accessの認証ページが表示される。

資料作成中にスマホでざっと練習したい、実際の挙動を確かめたい。しかしまだ公開前なので誰にでも状態にしたくないという場合に、簡単にメールアドレスによる認証をかけられて便利でした。

スライド作成を効率化するエディタ拡張機能やツール

エディタ上(自分の場合はVSCode)で作業することになるので、GitHub CopilotやCode Spell Checkerなどの普段開発で利用しているツールの恩恵を受けることができます。

VSCodeではMarkdownファイルでの補完がデフォルトでオフになっているため、Slidevの作業環境では次の設定を追加して、補完を有効にしておくと便利です。

"[markdown]": {
  "editor.quickSuggestions": true,
},

さらに効率的にスライドを作成するため、普段の開発のツールにプラスして、Slidevでスライドを生成する際に導入しておくと便利な拡張機能やツールを追加しておきます。

slidev-vscode

Slidevには公式のVSCode拡張があり、スライドのエディタ内プレビュー機能とアウトライン機能を備えています。プレビューがあることでエディタから画面を切り替えることなく、スライド作成作業を行うことができて便利です。

slidev-vscodeでスライドをエディタ内でプレビューする

アウトライン部分はクリックすることでそのページが記載されているファイルを開きます。長いスライドの場合は複数のマークダウンファイルに分割して作業をしているので、後から見直す際にこの機能が便利でした。

UnoCSS VSCode拡張

SlidevではUnoCSSと呼ばれる、TailwindCSSのスーパセットとなるAtomic CSSエンジンを利用してスタイリングできます(もちろん通常のCSSも利用可能です)。

UnoCSSのVSCode拡張により、実際の定義を参照できる。
UnoCSSのVSCode拡張により、実際の定義を参照できる。

UnoCSSはTailwind CSSよりもより短い記法(mx-4はmx4とかける)で記述できたり、Tailwindでは定義されていないクラス(例えばmx-13)でも指定できますが、指定できる幅が広すぎて実際に有効かどうかがわからない時があります。

この拡張機能を入れることで、有効な値についてはHoverヒントが表示されるようになり、書いてみたけどスタイルが当たらない、という事故を防ぐことができます。

左と右は、別のスライドディレクトリにおいた全く同じコンポーネント。テーマカラーに合わせてprimaryが変化している。
左と右は、別のスライドディレクトリにおいた全く同じコンポーネント。テーマカラーに合わせてprimaryが変化している。

また色を伴うプロパティの場合、プロパティ横に色のプレビューが表示されるようになります。この設定は各スライドディレクトリにある uno.config.ts の設定を読み込んでいるため、そのスライドのテーマに合わせた色を確認することができ、効率アップにつながります。

vscode-iconify + Iconify Raycast Extention

UnoCSSの機能の1つとして、 i-<iconifyのアイコンの名称> の形式でHTMLタグに属性を付与すると、アイコンを描画することができます。

Figmaやその他のスライドツールにWYSWIGで画像のレイアウトができないSlidevにおいて、テキストベースでスライドに彩りを加えることのできるアイコンはとても重宝します。

アイコンを使う際にいちいちIconifyのサイトに確認しにいくと効率が落ちるので、RaycastのIconify Extensionでアイコンを検索し、それをVSCode上に貼り付ける形で使用しています。

また長編のスライドになると、別の箇所で使用したアイコンをコピペで使用したいケースもあり、そういった際に vscode-iconifyというVSCode拡張を入れておくと、エディタ上でどんなアイコンかをプレビューできるのでおすすめです。

アイコンを実際にレンダリングするデモ。Iconifyにはプログラミング言語やフレームワークのアイコンも豊富にあるため、技術スタックの紹介を作成する際にも捗る。

動画内で使用している背景にアイコンを入れるレイアウトは、以下のようなVueコンポーネントで実装し、使いまわせるようにしています。

<script setup lang="ts">
/**
 * カバー画像としてアイコンを表示するコンポーネントです。
 * デフォルトではクエスチョンマークのアイコンが表示されます。
 */
defineProps<{
  icon?: string;
}>();
</script>

<template>
  <div text-30em absolute op5 right--25 top-10>
    <div :class="icon ?? 'i-fa6-regular:circle-question'" />
  </div>
</template>

UnoCSSでのスタイリングは valueless-attributify で行う

SlidevのUnoCSSではAttributify presetというプリセットが使えるようになっており、これにより以下のようにclass属性なしで直接スタイリングできます。

<!-- 通常の場合。Attributify presetと並行して利用可能 -->
<div class="text-xs text-red font-bold" />

<!-- valueless-attributifyを使用。直接属性として指定。 -->
<div text-xs text-red font-bold />

<!-- 同じ属性をまとめることも可能 -->
<div text="xs red" font-bold />

継続的に開発が行われるアプリには投入が躊躇われますが、ほぼ使い捨てのスライドデザインにおいては class=”” を省略できることで工数的に助かる面も大きかったです(直前になるとHTMLに的にセマンティックかどうかとかはガン無視で、マッチョなFlexやらGridでゴリ押しすることも多かったので…)。

Slidevのビルトインコンポーネントを使用する

Slidevにはあらかじめ用意されたコンポーネントがいくつかあり、中でもTransformコンポーネントは配置調整でよく使用しています。

Transformを使ったサンプル。コードブロック自体がTransformで囲まれていて、画面幅いっぱいではなく90%に縮小している。
Transformを使ったサンプル。コードブロック自体がTransformで囲まれていて、画面幅いっぱいではなく90%に縮小している。

そのほかに動画を埋め込むためのSlidevVideoコンポーネントなども使用していますが、一部コンポーネントについてはドキュメントに記載がないため、ソースコードを直接見ると良いかと思います。

再利用可能なスライドは共通ディレクトリに配置する

まだそこまで数は多くありませんが、自己紹介や締めの挨拶などの毎回同じ内容を掲載するページについては、リポジトリ直下の reuse ディレクトリに配置して読み込むようにしています。

---
src: '../../reuse/intro.md'
---

ちょっとしたTipsとして、自己紹介用の画像にはGitHubのアイコン取得用URL「 https://github.com/:user_id.png )を利用しています。

使い回している自己紹介スライド
使い回している自己紹介スライド

これは自己紹介用の画像を静的アセットとして配置する構成にすると、その画像を毎回スライドディレクトリのpublicディレクトリに配置しないといけないためです。

配信されているリソースに依存するため、スライド投影時にネットワークに繋がっていることが前提になりますが、オフライン環境にいることもあまりないので、管理コストの低いこちらの方法を採用しています。

伸び代ポイント

Markdownで管理している割に、構想からスライドまでシームレスにいかない

私は以下のような順序でスライドを作成しています。

  1. アウトラインを大小問わず全部書き出す
  2. アウトラインを並び替えたりこねくり回す
  3. 全体につながるいい感じのストーリーを考える
  4. アウトラインの1階層を見出しにスライドに起こす

Slidevのスライド作成は4に相当し、1~3までもテキストファイルです。しかしアウトラインとSlidevのシンタックスが一致するわけではないので、結局アウトラインを横目に1枚ずつ丹精込めてスライドに起こしています。これはなんだか無駄ですね。

所詮はテキストファイルに過ぎないので、適当なスクリプトを書いてアウトラインを自動でスライドに変換できるといいなと思っています。

- 見出し1
  - ほげ
  - ふが
  - ぴよ
- 見出し2

↓

---

# 見出し1

- ほげ
- ふが
- ぴよ

---

# 見出し2

さらにいうとアウトラインを作成してこねくり回す部分はこれまでTransnoというツールを使っていたのですが、だいぶ前にサポートが終了してしまったため、ここを含めてVSCode上で完結できないかな、と画策中です。

MarkdownファイルでVueコンポーネントの補完が効かない

Slidev内では独自に作成したVueコンポーネントをMarkdownファイルに埋め込むことができるのですが、そのパッケージ内で定義しているVueコンポーネントは当然ながら補完も効きませんし、コードジャンプもできません。そのためVueコンポーネントを使用する際は定義を見にいくか、都度Copilot頼みになります。

全編Markdownで頑張ればいいんですが、エディタに向かっているとついついコンポーネント化してしまい、一方でずっとスライドを書いていたりするわけでもないのでコンポーネントの使い方を忘れ、補完が効かなくて辛いなあというループを繰り返していました。

解決策として

  • Vue用のコンポーネントカタログライブラリ Histoire を導入し、コンポーネントリストを見られるようにしておく
  • Volar.jsを使い、Markdown上でVueのLSPが動作するようにする(Vueファイルの読み込み先ディレクトリは限定する必要がある)。

あたりをすればいい感じの体験にできるのではないかと思っていたりするのですが、いずれもまだ試せていません。

コンポーネントとテンプレートの使い分けをうまくできていない

共通のレイアウトが発生した際に、何も考えずにコンポーネントとして実装して、そのコンポーネントに適宜Propsを渡してレイアウトを作っていました。

Vueコンポーネントで扉絵のレイアウトを構成している例
Vueコンポーネントで扉絵のレイアウトを構成している例

上図はKaigi on Railsで実際に使用した資料の共通の扉スライドで、実装としては次のようになっています。

<script setup lang="ts">
defineProps<{
  title: string;
  headline?: string;
  description?: string;
  color?: string;
  icon?: string;
}>();
</script>

<template>
  <div flex="~ col" justify-center h-full px-4>
    <div v-if="headline" text="md white" font-700 bg-primary w-fit px-3 py-1 rounded>
      {{ headline }}
    </div>
    <div :class="color" text-6xl font-black pt4>
      {{ title }}
    </div>
    <div v-if="description" text-lg pl-1 pt4 font-700 text-slate-500>
      {{ description }}
    </div>
  </div>
  <SilhouetteIcon :icon="icon" />
</template>

<!-- 使用する側 -->
<ChapterDoor title="POSTを使ったアップロード ... />

一方でSlidevにはLayoutというこれまたVueファイルでレイアウトを定義できる機構があり、Layoutを利用する側はnamed slotを ::<slot name>:: という形式で参照することができます。

---
layout: chapter-door
---

::headline::

Chapter2

::title::

POSTを使ったアップロード

...

先のVueファイルに対する補完が効くかどうかにもよりますが、大まかなレイアウトをLayoutとして切り出して、細かいあしらいを都度コンポーネントとして定義して上乗せしていくのが良いかもしれません。

スライドアプリケーション形式で求められた際に対応できない

Slidevでは SPA、PDF、PNG、Markdown形式でのエクスポートはできますが、GoogleスライドやKeynoteなどの形式でエクスポートすることができません。

実際にあった話として、スライドアプリケーション形式が求められていることを認識せずにSlidevでスライドを作ってしまい、直前になって出力したPNGを人力でKeynoteに貼り付けたりしていました。

少し調べてみたところ、例えばGoogleスライドであればmd2googleslidesというツールで変換できそうです(が、 slidev export —format md の実行結果がなぜかpng画像だったので、ここから調整が必要かもしれません…)。

おわりに

今回はスライドをSlidevで作ってみた感想を書きました。

自分の運用が整理しきれておらず、ものすごく効率化できている訳ではありませんが、Hackableなツールなので工夫してより良い発表資料作成環境を整えたいと思います!

来年も登壇していくぞ!!

明日は Kodai Takagi さんが書きます!