0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare を使い倒す Slidev モノレポでホストする編

Posted at

みなさん、LT とか、テックカンファの登壇とか、しますか?
しますよね。

こういうカンファで使うスライドの作成ツールって、何を使いますか…??

私はもっぱら Slidev を使おうと思っているところです。(登壇することがないので…)

[宣伝]
Slidev の日本語版ドキュメントがフル更新されました!(私が頑張りました)
Slidevって英語ドキュメントしかないからなぁ…と思っていた方、是非日本語版で入門してみませんか?
Slidev 日本語版ドキュメント へダイブ!

さて、宣伝もここまでに、Slidev を Workers にホストして、みんながいつでも見られるようにしましょう!

なお、急いで書いているので、中身はスカスカです。リライト要請があったらリライトします。

アーキテクチャ

使うのは

  • Cloudflare Workers
  • GitHub Actions
  • Slidev

だけです!

Workers の解説

今回使うのは、Cloudflare Workers の静的アセット配信です。

Workers は本来 3MB くらいしか載せられません。

しかし、静的アセット (コンパイル・バンドル済みのファイル) を乗せる分には 20,000 ファイルまで、1 ファイル 25MiB まで、乗せることが出来ます。

これを、ふんだんに使います。

とはいえ、静的アセットをホストするだけじゃどうにもなりません。

ルーティングしないと、ってことで Workers をガッツリ書きます。

ルーティング方針

モノレポでやるってことは、1 つのレポジトリに複数のスライドプロジェクトを突っ込むってことです。

なので、ホスティング方針は /slide1//slidev2/ と ... としたいですよね。

できます。大丈夫です。
単純にホスティングディレクトリ直下のディレクトリ構造は全部キープされるので、それぞれのディレクトリに成果物を入れれば ok です。

ってことで、ホスティングしてるディレクトリ直下のアクセス方法を以下のようにして、Workers にルーティングしてもらいます。

src/index.ts
export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    const path = url.pathname;

    // 静的アセット(.html, .css, .js, /assets/)は直接配信
    if (
      path.endsWith(".html") ||
      path.endsWith(".css") ||
      path.endsWith(".js") ||
      path.endsWith(".json") ||
      path.includes("/assets/") ||
      path.match(/\.\w+$/) // 拡張子あり
    ) {
      return env.ASSETS.fetch(request);
    }

    // SPA ルーティング:拡張子なしなら index.html にリライト
    const baseMatch = path.match(/^\/([^/]+)/); // ← スライドの base を抽出
    if (baseMatch) {
      const base = baseMatch[1];
      const indexUrl = new URL(`/${base}/index.html`, url.origin);
      return env.ASSETS.fetch(indexUrl);
    }

    // デフォルト
    return env.ASSETS.fetch(request);
  },
};

あとは、wrangler.jsonc も更新しましょう。

wrangler.jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "slidev",
  "main": "src/index.ts",
  "compatibility_date": "2025-11-23",
  "workers_dev": false,
+ "assets": {
+   "directory": "./dist",
+   "binding": "ASSETS",
+   "not_found_handling": "single-page-application"
+ }
}

ビルド方針

Slidev はビルド時に、ベースパスを渡せます。要は、ビルドしたやつは基本的に ./ がベースパスになるので、例えば slide1./slide1/hogehoge.js とかはアクセスできません。(ルートパス直下を指すため、 ./hogehoge.js を読み出してしまう → 結局 ./hoge.jshttps://example.com/slide1/hoge.js ではなく、https://example.com/hoge.js をリクエストしてしまうので、動かない)

そこで、ビルド時にベースパスを書き換えます。

scripts/build.ts
import { dirname } from 'node:path'
import { x } from 'tinyexec'
import { execSync } from 'node:child_process'
import fg from 'fast-glob'
import { existsSync } from 'node:fs'
import { cp } from 'node:fs/promises'

const packageFiles = (await fg('slides/!(_template)/package.json', { onlyFiles: true })).sort()

const allSlides = await Promise.all(
  packageFiles.map(async (file) => {
    const slideRoot = dirname(file)
    return { dir: slideRoot  }
  })
)

const getChangedSlides = () => {
  try {
    const diff = execSync('git diff --name-only HEAD~1 HEAD', { encoding: 'utf-8' }).toString().trim().split('\n')

    const changedSlides = new Set(
      diff.filter(file => file.startsWith('slides/')).map(file => file.split('/')[1])
    )

    return Array.from(changedSlides)
  } catch {
    return null
  }
}

const changedSlides = getChangedSlides()
const slides = allSlides.filter(s => {
  const distStalePath = `dist-stale/${s.dir.replace('slides/', '')}`

  if (changedSlides) {
    return changedSlides.includes(s.dir.replace('slides/', ''))
  }

  if (!existsSync(distStalePath)) return true;
  return false;
})

await Promise.all(
  slides.map(async (slide) => {
    console.log(`Building slide in ${slide.dir}...`);
    const buildCommand = [
      "slidev",
      "build",
      "--base",
      `/${slide.dir.replace("slides/", "")}/`,
      "--out",
      `../../dist-stale/${slide.dir.replace("slides/", "")}`,
    ];

    await x("pnpm", [...buildCommand], {
      nodeOptions: { cwd: slide.dir, stdio: "inherit" },
    });
  })
)

await cp('dist-stale/', 'dist/', { recursive: true })

以上のコードでは、キャッシュ戦略とかいろんなことをやっていますが、説明は端折ります。許してください…

で、ビルドコマンドを書きます。

package.json
{
    "scripts": {
        // ...
+       "build": "rimraf dist && tsx scripts/build.ts"
        // ...
    }
}

GitHub Actions を使ったデプロイ

.github/workflows/deploy.yml
name: Deploy Slides
on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy Slidev Slides to Cloudflare Workers
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '24'
      - name: Install dependencies
        run: npm install -g pnpm && pnpm install
      - name: Build Slidev Slides
        run: pnpm build
      - name: Publish to Cloudflare Workers
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: deploy
      - name: Post Deployment Message
        run: echo "Slidev slides have been successfully deployed to Cloudflare Workers!"

これを実行するうえで、GitHub Actions の Secret を設定してください。→ 詳しくはこちら

できあがり

端折りすぎましたが、Done is better than perfect を都合良く解釈しましょう。

とにかく説明したかったのは

  • ルーティング方針
  • ビルド方針

です!
この 2 つがわかっていれば、Slidev をデプロイできます!

ビバ Slidev ライフ!

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?