💡この記事で分かること
- Next.js 14 App Routerでのファイルパス問題の解決
- @react-pdf/rendererの日本語フォント対応
- AI開発ツール(Cursor)を使った問題解決のプロセス
対象読者: Next.jsでPDF生成機能を実装したい方、日本語フォントの文字化けやエラーで困っている方
はじめに
株式会社Dirbatoの社内横断技術支援組織「Backbeat」で、最新技術の調査や社内ツールの保守開発を担当している山田です!
私はAI IDE「Cursor」を使って共同旅行計画アプリ「PonChord」を個人開発しています。今回は、開発中に特に苦労したPDF生成機能の実装について、初心者ならではのAI開発での苦悩と解決方法を共有します!
ReactでPDFを作るなら@react-pdf/rendererがデファクトスタンダード……とCursorに教えてもらいましたが、いざ実装してみると日本語対応という大きな壁が立ちはだかりました。。
この記事の対象環境
- Next.js 14 (App Router)
- @react-pdf/renderer v3.x
- TypeScript
- Cursor (AI IDE)
「PonChord」個人開発についてのQiita記事
- 【Next.js 14 × Cursor】知識ゼロからAIとペアプロして、旅行計画アプリ「PonChord」を個人開発した話
- 【Next.js 14 × Cursor】初心者がparams非同期化とPrisma generateでハマった話と解決法
- 【React-PDF × Next.js】日本語PDF生成の文字化け地獄をなんとかした話 ⇐今回の内容
前提知識:@react-pdf/rendererとは
ReactでPDFファイルを作成できるライブラリです
- Reactコンポーネントを書く感覚でPDFが作れる
- 請求書、レポート、チケットなどの出力に最適
- CSS-likeなスタイリングで見た目を調整可能
- 動的データの埋め込みや条件分岐も簡単
導入方法
npm install @react-pdf/renderer
基本的な使い方
import文
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
主要なコンポーネントの役割:
-
Document→ PDF全体の入れ物(ルートコンテナ) -
Page→ PDFの1ページ(A4、Letterなどサイズ指定可能) -
View→ レイアウト用(HTMLの<div>みたいなもの) -
Text→ 文字を表示(テキストはこのコンポーネント内に書く必要がある) -
Image→ 画像の埋め込み -
Link→ クリック可能なリンク
コード例
import { Document, Page, Text, View, PDFDownloadLink } from '@react-pdf/renderer';
// PDFドキュメント
function MyDocument() {
return (
<Document>
<Page size="A4">
<View>
<Text>こんにちは!ReactでPDFを作成</Text>
</View>
</Page>
</Document>
);
}
// ダウンロードボタン
function App() {
return (
<PDFDownloadLink document={<MyDocument />} fileName="sample.pdf">
{({ loading }) => (loading ? 'PDF生成中...' : 'PDFをダウンロード')}
</PDFDownloadLink>
);
}
(だがしかし)日本語対応の落とし穴
デフォルトでは日本語に対応していませんでした、、!日本語を表示するには日本語フォントを自分で登録する必要がありました、、
これを知らずに実装すると、次のセクションで紹介する「文字化け地獄」が待っています...
文字化け問題発生
Cursorに「PDF出力機能導入したい!」って簡単にプロンプトを投げて書いてもらったコードで生まれた化け物、、、
何が起きたのか?
- 日本語部分が全て文字化けに
- 英数字は正常に表示されている
- PDFの構造自体は問題なく生成されている
つまり、フォントの問題だということが分かります。
最初の実装を確認してみる
確認したらこんな感じでした
Font.register({
family: 'NotoSansJP',
src: '/fonts/NotoSansJP-Regular.ttf',
})
何が問題だったのか?
@react-pdf/rendererのFont.registerのsrcは、公式ドキュメントによると 「有効なURL(ブラウザ)」 または 「Node利用時の絶対パス」 を指定する仕様になっています。
つまり、/fonts/...のようなパス指定自体は仕様上は問題ないはずなのですが、Next.js App Routerの環境では思わぬ落とし穴がありました。
Next.js App Routerでハマったポイント
Next.js 14のApp Routerでは、コンポーネントはデフォルトでServer Componentsとして扱われます。
私の環境では以下の問題が発生していました:
-
Server Component側で
Font.registerが実行されてしまう- Node.jsのパス解決が走り、ブラウザ用のURL(
/fonts/...)が正しく処理されない
- Node.jsのパス解決が走り、ブラウザ用のURL(
-
バンドラーの最適化が効かない
- Next.jsのWebpack/Turbopackによる最適化の恩恵を受けられない
「srcって書いてあるから、単純にパス書けばいいんでしょ?」という感じの実装になっていました(AIあるあるですね)
Cursorとの試行錯誤の日々
AIに追加で条件を与えて、何度も再提案してもらいました。
試行錯誤の過程
- 最初の試み: publicフォルダのパス指定 → ❌ 失敗
- 2回目: クライアント側の絶対パスを指定 → ❌ 失敗
- 3回目: Base64エンコード方式 → ✅ 成功!(でも...)
Cursorが提案してくれた実装
'use client'
import { useEffect, useState } from 'react'
import { Font } from '@react-pdf/renderer'
import { arrayBufferToBase64 } from '@/lib/pdf-helper'
export default function PDFComponent() {
const [fontLoaded, setFontLoaded] = useState(false)
useEffect(() => {
const loadFont = async () => {
const response = await fetch('/fonts/NotoSansJP-Regular.ttf')
const buffer = await response.arrayBuffer()
const base64 = arrayBufferToBase64(buffer)
Font.register({
family: 'NotoSansJP',
src: `data:font/ttf;base64,${base64}`,
})
setFontLoaded(true)
}
loadFont()
}, [])
// ... PDF生成処理
}
この実装の何が問題だったのか
動作はするものの、Next.jsの最適化を活かせずパフォーマンスとメンテナンス性に問題がありました。
問題点:
-
コードが複雑 → 非同期処理と状態管理(
fontLoaded)が必要 - パフォーマンス低下 → Base64変換でファイルサイズが約33%増加
- ビルド最適化が効かない → Next.jsのバンドラーによる最適化の恩恵を受けられない
でもそんなことは知らず、この実装で一旦日本語出力されて安堵した私。
いったんちゃんと出力されたし、上司に自慢しよ〜
私: 「できました!日本語PDF出力できました!」
上司: 「本来はFontを落としてくるのは通信量が増えるのであまりよろしくないし、サーバーサイドでやるのが良いと思う。
まぁ今回は良いかな?もしクライアントサイドでやるならbase64で読み込むのではなくてこの記事を参考にすると良いんじゃない??」
これ正しい実装方法になっていないの、、??(ショック)
そして教えてもらったサイトを確認し、実装してみることにしました。
ベストプラクティスな実装方法
教えてもらったMoney Forward Developers Blogの記事を参考に、Next.jsに最適化された実装方法を学びました。
Step 1:フォントファイルの準備
日本語フォント(Noto Sans JP)の.ttfファイルをsrc/fonts/フォルダに配置します。
src/
└── fonts/
└── NotoSansJP-Regular.ttf
重要なポイント:
-
publicフォルダではなくsrcフォルダに配置 - これによりバンドラー(Webpack/Turbopack)が適切に処理してくれる
Step 2:フォントの登録
// src/components/PDFDocument.tsx
'use client'
import { Font } from '@react-pdf/renderer'
import NotoSansJP from '@/fonts/NotoSansJP-Regular.ttf'
// フォントを登録
Font.register({
family: 'NotoSansJP',
src: NotoSansJP, // importしたフォントファイルを直接指定
})
この方法の利点:
- Base64変換が不要(バンドラーが自動処理)
- コードがシンプルで読みやすい
- Next.jsのビルドプロセスに最適化されている
- TypeScriptの型チェックも効く
use clientディレクティブは必須
Next.js 14のApp Routerでは、コンポーネントはデフォルトでサーバーコンポーネントとして扱われます。
PDF生成には以下の処理が必要なため、必ず'use client'を指定してください:
-
Font.register()によるブラウザメモリ上でのフォント登録 -
PDFDownloadLinkなどのクライアントサイドでのPDF生成 - ユーザーのクリックイベントやダウンロード処理
use clientを忘れると、「useState/useEffectが使えない」「Font.registerが動作しない」などのエラーが発生します!
Step 3:TypeScript型定義の追加
フォントファイルをimportするために、型定義を追加します。
// src/types/fonts.d.ts
declare module '*.ttf' {
const content: string
export default content
}
Step 4:PDFドキュメントでフォントを使用
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'
const styles = StyleSheet.create({
page: {
fontFamily: 'NotoSansJP', // 登録したフォントを指定
fontSize: 11,
padding: 40,
},
// ... その他のスタイル
})
const MyPDFDocument = () => (
<Document>
<Page size="A4" style={styles.page}>
<View>
<Text>日本語が表示されます!</Text>
</View>
</Page>
</Document>
)
結果
無事日本語PDFが出力されました!! やった〜!!!
PDFの表示形式はまだ検討中ですが、スケジュールが時系列順に出力されています。
改善されたポイント:
- 日本語が正しく表示される
- フォントの読み込みが安定している
- コードがシンプルで保守しやすい
- Next.jsのベストプラクティスに準拠
まとめ
@react-pdf/rendererは非常に強力ですが、日本語フォント対応には少しコツがいります。
重要なポイント
1. フォントはsrcフォルダに配置し直接import
import NotoSansJP from '@/fonts/NotoSansJP-Regular.ttf'
Font.register({ family: 'NotoSansJP', src: NotoSansJP })
- Next.jsのバンドラーが自動最適化
- Base64変換不要でコードがシンプル
2. use clientディレクティブを忘れずに
- App RouterではデフォルトがServer Components
- PDF生成はクライアントサイドで行う必要がある
3. AIの提案は公式ドキュメントや信頼できる情報源で確認
- AIは汎用的な解決策を提示するが、フレームワーク固有のベストプラクティスを知らないことが多い
- 必ず公式ドキュメント・信頼できる技術記事・コードレビューで確認する!
学んだこと
今回の実装を通じて、「動けばいい」と「ベストプラクティス」の違いを痛感しました。
AIツールは強力ですが、提案されたコードが本当に最適かどうかは、自分で判断する必要があります。特にNext.jsのようなフレームワークでは、フレームワークの思想に沿った実装をすることで、パフォーマンスやメンテナンス性が大きく変わってきます。
「AIに聞けば何でも解決!」ではなく、「AIの提案を理解して、より良い方法を探す」姿勢が大切だと学びました。

