[Remix] Reactで書いたページをPDFで返すサーバーを作る

# はじめに

React で書いたページをそのまま PDF として返すサーバーを作ってみようと思いググったのですが、デファクトスタンダードな方法が見つかりませんでした。
そこで、色々試してみて動く形になったものを公開します。
もし他に良い方法があったらコメントで教えてくださると嬉しいです。

# やりたいこと

やりたいこととしては、

1. PDF 化するページを React で記述したい
1. 1. で作ったページを PDF でダウンロードできるようにしたい

です。
サーバーとして動作するものとし、`/` へのアクセスで 1. のページを表示し、`/pdf` へのアクセスで 2. の PDF をダウンロードできるようにします。

# 手順と解説

プロジェクトを作成するところから解説します。

### 1. Remix サーバーの作成

まずは[Remix 公式の Quick Start](https://remix.run/docs/en/main/start/quickstart)にある通り下記コマンドでプロジェクトを作成します。

```shell
npx create-remix@latest
```

対話形式でプロジェクトを作成します。
途中 npm を使うか聞かれるのですが、私は pnpm を使いたかったので No にしました。

```shell
> Install dependencies with npm?
> No
```

プロジェクトができたら、パッケージをインストールします。

```shell
pnpm i
```

そして開発サーバーを起動。

```shell
pnpm dev
```

これで、`http://localhost:5173` にアクセスすれば初期ページが表示されます。

一応ビルドが通って本番サーバーが起動できるかも確認しておきます。

```shell
pnpm build
pnpm start
```

`http://localhost:3000` にアクセスで動作確認できます。

### 2. 細かい設定

開発サーバーも 3000 ポートで起動したかったので、`package.json` の `scripts` を下記のように書き換えました。

```diff_json
  "scripts": {
    "build": "remix vite:build",
-    "dev": "remix vite:dev",
+    "dev": "remix vite:dev --port 3000",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve ./build/server/index.js",
    "typecheck": "tsc"
  },
```

これで `pnpm dev` 時も `http://localhost:3000` でアクセスできるようになります。

### 3. PDF を返すエンドポイントを作成

`/pdf` へアクセスすると、`/` の内容を PDF 化したものをレスポンスするようにします。

まず、html を PDF に変換する [puppeteer](https://www.npmjs.com/package/puppeteer) を dependencies に追加します。

```shell
pnpm add puppeteer
```

次に `/app/routes/pdf.tsx` を追加します。
routes ディレクトリ配下に追加するだけで Remix が検知して `/pdf` エンドポイントを作成してくれます。

```/app/routes/pdf.tsx
import puppeteer from "puppeteer";

export async function loader() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto("http://localhost:3000", { waitUntil: 'networkidle0' });
  const pdf = await page.pdf({ format: 'A4' });
  await browser.close();

  return new Response(pdf, {
    status: 200,
    headers: {
      "Content-Type": "application/pdf",
      'Content-Disposition': 'attachment; filename=remix.pdf',
    },
  })
}
```

一旦これで完了です。
開発サーバーか本番サーバーを起動し、`http://localhost:3000/pdf` にアクセスしてみてください。
`http://localhost:3000` にアクセスした時と同じ内容が PDF でダウンロードできたかと思います。

動作としては、`http://localhost:3000/pdf` にアクセスすると内部で `http://localhost:3000` にリクエストを飛ばし、取得した html を puppeteer で PDF 化して返す、という流れになります。

### 4. フォント設定

自前のフォントファイルを使用したい場合の設定です。
まずフォントファイル（今回は `NotoSerifJP.ttf`）を `/public` ディレクトリの下におきます。
（`/public` ディレクトリは、`npx create-remix@latest` を実行したときに自動で作成されているはずです）

次に、フォントを設定する css ファイルを作成し、`/app` ディレクトリの下におきます。
今回は `/app/base.css` とし、下記内容で作成しました。

```/app/base.css
@font-face {
    font-family: 'NotoSerifJP';
    src: url('/NotoSerifJP.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
}

body {
    font-family: 'NotoSerifJP', sans-serif;
}
```

最後に `/app/root.tsx` にインポートを追加します。

```diff_tsx
import "./tailwind.css";
+ import "./base.css";
```

# おわりに

これで、`http://localhost:3000` で画面表示を確認しつつ、`http://localhost:3000/pdf` で PDF をダウンロードできるようになりました。
ちなみに [React-pdf](https://react-pdf.org/) も試したのですが、これは独自のタグを使って 1 から PDF を組み立てていくような感じで、設定できる style も限られていたので今回は見送りました