Next.js + react-pdfを利用 iPhoneでPDF1ページしか表示されない解決方法

react-pdf 実装

iPhone Safari での PDF 全ページ表示問題を解決するため、<iframe> を使った PDF 表示を廃止し、react-pdf を用いた全ページレンダリングに切り替えた。 iframe では、 iOS Safari において PDF が1ページ目しか表示されなかった。

使用パッケージとバージョン

パッケージバージョン備考
react-pdf^10.4.1Document / Page コンポーネント提供
pdfjs-dist^5.4.296react-pdf が内部で使うバージョンに合わせる(重要)

インストール

pnpm install react-pdf pdfjs-dist@5.4.296

pdfjs-dist のバージョンは必ず react-pdf が要求するものに揃えること。 バージョンが異なると後述のバージョン不一致エラーが発生する。

遭遇したエラーと解決

エラー 1: DOMMatrix is not defined(SSRエラー)

エラー内容:
const SCALE_MATRIX = new DOMMatrix();
digest: ‘534975899’

原因: pdfjs-dist のモジュール初期化時に DOMMatrix(ブラウザ専用API)を使用しているため、 Next.js の SSR フェーズ(Node.js 環境)でクラッシュする。
'use client' を付けてもモジュールの初期化はサーバーで走るため回避できない。

解決策: react-pdf を使うコンポーネントを別ファイル(PdfViewer.tsx)に分離し、next/dynamic の ssr: false でクライアントのみ読み込む。

// IntroMediaOverlay.tsx
import dynamic from 'next/dynamic';

const PdfViewer = dynamic(
  () => import('@/components/PdfViewer'),
  { ssr: false }
);

エラー 2: API version does not match Worker version

エラー内容:
UnknownErrorException: The API version "5.4.296" does not match the Worker version "5.5.207".

原因: package.json に直接インストールされた pdfjs-dist が 5.5.207 だったのに対し、react-pdf 内部が使う pdfjs-dist は 5.4.296 だった。 Worker ファイルの解決に 5.5.207、API には 5.4.296 が使われバージョン不一致になった。

解決策: pdfjs-dist を react-pdf が要求するバージョンに固定する。

pnpm install pdfjs-dist@5.4.296

確認方法:

cat node_modules/react-pdf/package.json | grep pdfjs-dist
# → "pdfjs-dist": "5.4.296"

実装コード

PdfViewer.tsx(react-pdf 本体)

// src/components/PdfViewer.tsx
'use client';

import { useRef, useState, useCallback } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString();

export default function PdfViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState<number>(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState<number>(600);

  const onDocumentLoadSuccess = useCallback(
    ({ numPages }: { numPages: number }) => {
      setNumPages(numPages);
      if (containerRef.current) {
        setContainerWidth(containerRef.current.clientWidth);
      }
    },
    []
  );

  return (
    <div ref={containerRef} className="overflow-y-auto max-h-[60vh] p-2">
      <Document
        file={url}
        onLoadSuccess={onDocumentLoadSuccess}
        loading={<span>PDF読み込み中...</span>}
      >
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i}
            pageNumber={i + 1}
            width={containerWidth - 16}
            className="mb-2"
          />
        ))}
      </Document>
    </div>
  );
}
Win版 Jsree スクショ 無音ガメラ Web版

Next.jsで「/#/」を使う方法|ハッシュルーティングの仕組みと実装例

Next.jsで「/#/」を使う方法|ハッシュルーティングの仕組み

1. 「/#/」とは?ハッシュルーティングの基本

ブラウザURLで見かける /#/ は、「ハッシュルーティング(Hash Routing)」と呼ばれる仕組みです。 これはシングルページアプリケーション(SPA)で、ページ遷移をクライアント側のみで制御するために使われます。

例えば https://mjeld.com/#/user/123 のようなURLでは、#以降の部分はサーバーには送信されません。 そのためサーバー設定を触らなくても、フロントエンドだけでルートを切り替えられるという利点があります。

2. Next.jsでハッシュルーティングを実装する

Next.jsは通常「ファイルベースルーティング(/pages や /app ディレクトリ)」を採用していますが、 どうしても /hash スタイルを使いたい場合は、クライアント側で window.location.hash を監視してルーティング処理を行います。

実装例(app/page.tsx)

'use client';

import { useEffect, useMemo, useState } from 'react';

type Route = { name: string; param?: string };

function parseHash(hash: string): Route {
  // 例: #/ , #/about , #/user/123
  const raw = hash.startsWith('#') ? hash.slice(1) : hash;
  const path = raw.replace(/^\/+/, ''); // 先頭の / を除去
  const seg = path.split('/').filter(Boolean);

  if (seg.length === 0) return { name: 'home' };
  if (seg[0] === 'about') return { name: 'about' };
  if (seg[0] === 'user' && seg[1]) return { name: 'user', param: seg[1] };
  return { name: 'notfound' };
}

export default function Page() {
  const [hash, setHash] = useState<string>(typeof window !== 'undefined' ? window.location.hash : '#/');

  useEffect(() => {
    const onChange = () => setHash(window.location.hash || '#/');
    window.addEventListener('hashchange', onChange);
    return () => window.removeEventListener('hashchange', onChange);
  }, []);

  const route = useMemo(() => parseHash(hash), [hash]);

  return (
    <main className="max-w-2xl mx-auto p-6 space-y-6">
      <nav className="flex gap-3">
        <a href="#/">Home</a>
        <a href="#/about">About</a>
        <a href="#/user/123">User 123</a>
      </nav>

      {route.name === 'home' && <h1 className="text-2xl font-bold">Home</h1>}
      {route.name === 'about' && <h1 className="text-2xl font-bold">About</h1>}
      {route.name === 'user' && (
        <section>
          <h1 className="text-2xl font-bold">User</h1>
          <p>ID: {route.param}</p>
        </section>
      )}
      {route.name === 'notfound' && <p>Not Found</p>}
    </main>
  );
}

このコードでは、hashchangeイベントを利用して#以降の変化を検知し、 表示するコンポーネントを切り替えています。

3. ハッシュルーティングを使うメリット・デメリット

項目メリットデメリット
設定の手軽さサーバー設定不要でSPA動作可
404対策リロードしても404にならない
SEO検索エンジンは#以降を無視しがち
URLの見た目古い設計に見えることがある

4. Next.jsでおすすめのルーティング方法

もしサーバー側(Vercel・Amplify・Cloudflare Pagesなど)でリライト設定が可能なら、 ハッシュ方式ではなく通常の/users/123形式を推奨します。

PowerShellでNode.jsの最新バージョン一覧を確認する方法【Volta/Windows対応】

Next.jsでbasePathを/homepage2にしてS3へ静的デプロイする完全手順

pnpmとは?npmとの違いとインストール方法

Next.jsのlayout.tsxで複数サイズのPNGファビコンを指定する方法

AWS Amplify CLIでS3やZIPから手動デプロイ【実行コマンド付き】