1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js (Pages Router) で react-pdf を動かしてみた

Last updated at Posted at 2024-09-27

はじめに

今回試したソースコードはこちらで確認いただけます。

やってみたこと

正しい表現か自信はありませんが、ニュアンスを感じ取っていただければ幸いです。

  • クライアントサイドでPDFをレンダリングする(CSR)
  • サーバーサイドでPDFを生成して、クライアントサイドからは埋め込みで閲覧する(SSR)
  • API RouteのレスポンスとしてPDFファイルを表示する

今回検証に使用したツール類のバージョンはこちらです。

Name Version
M2 MacBook Air 2022 14.6.1
Google Chrome 129.0.6668.70
Node.js 20.11.0
npm 10.2.4
@react-pdf/renderer 4.0.0
next 14.2.13
react 18.3.1
typescript 5.6.2

環境構築

まずは Next.js のテンプレートからリポジトリを作成します。

  • Biomeを使ってみたかったのでESLintはNo
  • 簡単にお試しするだけなので、UtilityFirstなTailwindを選択
  • AppRouterは今回使いませんのでNo
  • その他は適当です
$ npx create-next-app@latest
Need to install the following packages:
create-next-app@14.2.13
Ok to proceed? (y) y
✔ What is your project named? … react-pdf-with-next-pages-router
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … No
✔ Would you like to customize the default import alias (@/*)? … Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/user/workspace/react-pdf-with-next-pages-router.
依存ライブラリを最新化しておく
npm i react@latest react-dom@latest next@latest typescript@latest \
  @types/node@latest @types/react@latest @types/react-dom@latest \
  postcss@latest tailwindcss@latest

package.json

  "dependencies": {
    "next": "^14.2.13",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@biomejs/biome": "1.9.2",
    "@types/node": "^22.6.1",
    "@types/react": "^18.3.9",
    "@types/react-dom": "^18.3.0",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.13",
    "typescript": "^5.6.2"
  }

Biome導入

検証ついでに以前から気になっていたBiomeを使ってみます。ESLint、Prettier と高い互換性を持つツールチェーンです。

ライブラリを導入してinitします。

npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init

package.json 内のscriptsを編集します。

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
-   "lint": "next lint",
+   "lint": "biome lint",
+   "format": "biome format",
+   "check": "biome check --write"
  },

Cursor(VSCode)拡張機能のインストール

# VSCode
code --install-extension biomejs.biome
# Cursor
cursor --install-extension biomejs.biome

.vscode/settings.json

{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

まだコード量が少ないので、速度に関してはESLint+Prettierとの差を体感することはできませんでした。ただ、使い勝手は全然変わらないですね。乗り換えのハードルはかなり低そうです。

react-pdf

npm install @react-pdf/renderer --save

PDFを出力してみる

PDFDocumentコンポーネントを作っておく

src/components/PDFDocument.tsx

import { Document, Page, Text, View } from "@react-pdf/renderer";
import { StyleSheet } from "@react-pdf/renderer";

// Create styles
export const styles = StyleSheet.create({
  page: {
    fontFamily: "HackGen35",
    flexDirection: "row",
    backgroundColor: "#E4E4E4",
  },
  section: {
    margin: 10,
    padding: 10,
    flexGrow: 1,
  },
  boldText: {
    fontWeight: "bold",
  },
});

/**
 * Create Document Component
 */
const PDFDocument = () => (
  <Document>
    <Page size="A4" style={styles.page}>
      <View style={styles.section}>
        <Text>Section #1</Text>
        <Text>こんにちは</Text>
      </View>
      <View style={styles.section}>
        <Text>Section #2</Text>
        <Text style={styles.boldText}>太字です</Text>
      </View>
    </Page>
  </Document>
);

export default PDFDocument;

日本語が文字化けすることへの対処

Fontを登録してあげないと、日本語文字が文字化けする可能性があります。
公式ドキュメントでは TTFフォントファイルのみサポートしている記述がありますが、Issue Commentを見る限りWOFFファイルも対応しているようです。
今回はプログラミング向けフォントとして公開されている 白源 (はくげん/HackGen) を使ってみました。

なお、 public フォルダ内の静的Assetにアクセスするパスが処理が動く環境によって異なるようで、PDFレンダリング処理ごとに設定を呼び分けています。(詳細は各コードを参照)

リンクボタンを作っておく(任意)

src/pages/index.tsx に以下を追加しておきます。これでTopページから各コンポーネントにアクセスしやすくなります

<div className="gap-2 flex">
  <Link href={"/client"}>
    <Button>Client</Button>
  </Link>
  <Link href={"/server"}>
    <Button>Server</Button>
  </Link>
  <Link href={"/api/pdf"}>
    <Button>/api/pdf</Button>
  </Link>
</div>

Client Side でPDFをレンダリング

src/pages/client/index.tsx

"use client";

import PDFDocument from "@/components/PDFDocument";
import { Font } from "@react-pdf/renderer";
import dynamic from "next/dynamic";

Font.register({
  family: "HackGen35",
  fonts: [
    {
      src: "/fonts/HackGen35-Regular.ttf",
      fontStyle: "normal",
      fontWeight: "normal",
    },
    {
      src: "/fonts/HackGen35-Bold.ttf",
      fontStyle: "normal",
      fontWeight: "bold",
    },
  ],
});

const DynamicPDFViewer = dynamic(
  () => import("@react-pdf/renderer").then((mod) => mod.PDFViewer),
  {
    loading: () => <p>Loading...</p>,
    ssr: false,
  }
);

/**
 * Client Side でPDFをレンダリングする
 */
const Page = () => {
  return (
    <>
      <p>クライアントサイドで作成したPDFを表示しています。</p>
      <DynamicPDFViewer className="mx-auto" width={1200} height={1000}>
        <PDFDocument />
      </DynamicPDFViewer>
    </>
  );
};

export default Page;

http://localhost:3000/client にアクセスするとPDFがレンダリングされて表示されます。
ClientSide.png

Lazy Loading (Doc)のssr optionをfalseとすることで、サーバーでのレンダリングを抑制しフロントで処理させています。

@react-pdf/renderer からexportされている PDFViewer コンポーネントは内部的にWebAPIを使用しているようで、Node.js環境で実行するとエラーが発生します。

Server Side でPDFを生成

src/server/pdf-font.ts

import path from "node:path";
import { Font } from "@react-pdf/renderer";

/**
 * サーバーサイドからpublicフォルダ内の静的ファイルにアクセスするための絶対パスを取得する
 */
export const getPublicAssetPath = (fileName: string): string =>
  path.resolve("./public", fileName);

Font.register({
  family: "HackGen35",
  fonts: [
    {
      src: getPublicAssetPath("fonts/HackGen35-Regular.ttf"),
      fontStyle: "normal",
      fontWeight: "normal",
    },
    {
      src: getPublicAssetPath("fonts/HackGen35-Bold.ttf"),
      fontStyle: "normal",
      fontWeight: "bold",
    },
  ],
});

src/server/makePDF.tsx

import "@/server/pdf-font";
import PDFDocument from "@/components/PDFDocument";
import { getPublicAssetPath } from "@/server/pdf-font";
import * as ReactPDF from "@react-pdf/renderer";

/**
 * サーバーサイドでPDFを生成し、publicフォルダにファイルとして出力する
 */
export const makePDF = async () => {
  const pdfFileName = "generated-in-server.pdf";
  await ReactPDF.renderToFile(<PDFDocument />, getPublicAssetPath(pdfFileName));
  return { fileName: pdfFileName, url: `/${pdfFileName}` };
};

src/pages/server/index.tsx

import { makePDF } from "@/server/makePDF";
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";

const Page = ({
  fileName,
  fileURL,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  return (
    <>
      <p>サーバーサイドで作成したPDFを表示しています。</p>
      <object
        className="mx-auto"
        title={fileName}
        data={fileURL}
        type="application/pdf"
        width={1000}
        height={1200}
      />
    </>
  );
};

export const getServerSideProps = (async () => {
  const { fileName, url } = await makePDF();
  return { props: { fileName, fileURL: url } };
}) satisfies GetServerSideProps<{ fileName: string; fileURL: string }>;

export default Page;

http://localhost:3000/server にアクセスするとPDFがレンダリングされて表示されます。clientの時と見た目はほぼ一緒ですが、PDFファイルの生成がサーバーサイドで行われている点が異なります。
ServerSide.png

getServerSideProps からは JSON serializable data types しかreturnできないっぽいので、ファイルのStreamやBufferをコンポーネントpropsに渡すことは出来なさそうでした。

API RouteのレスポンスとしてPDFファイルを表示する

src/pages/api/pdf.tsx

import PDFDocument from "@/components/PDFDocument";
import ReactPDF from "@react-pdf/renderer";
import type { NextApiRequest, NextApiResponse } from "next";
import "@/server/pdf-font";

export default async function handler(
  _req: NextApiRequest,
  res: NextApiResponse<never>
) {
  const stream = await ReactPDF.renderToStream(<PDFDocument />);
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader(
    "Content-Disposition",
    'inline; filename="react-pdf-sample.pdf"'
  );
  stream
    .pipe(res)
    .on("end", () => console.log("Done streaming, response sent."));
}

Streamを使用してレスポンスします。
http://localhost:3000/api/pdf にアクセスすると、サーバーで生成されたPDFがフルスクリーンで表示されます。
APIRoute.png

おわりに

クライアントサイド、サーバーサイドを選ばずPDF生成できるのでかなり便利だなと思いました。
React-pdf が公開するPDFのComponentsやStylingに関しては検証できていないので、また時間がある時に触ってみようと思います。

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?