リベロジックのアクセシビリティ診断サービスでは証明書・レポートをHTML形式で提供していましたが「証明書をPDF形式で提供してほしい」という声にお応えし、PDF形式の証明書とレポートを発行できるようになりました!
この対応にあたり、ビルドプロセスでHTMLを出力するのと同時にPDFを自動出力するシステムを構築しました。
🛠️ PDF化の基盤:Puppeteerと環境構築
PDF生成ツールとして、Chromiumブラウザをヘッドレスで操作できるPuppeteerを採用しました。もともとHTMLはAstroで構築されているため、Puppeteerの導入は比較的スムーズでした。
1. ローカルサーバーによるレンダリング環境の確保
PuppeteerはChromiumの印刷機能を利用しますが、ローカルのHTMLファイル(file://)を直接読み込むと、レイアウトが崩れてしまう問題が発生します。
これを回避するため、以下の環境を構築しました。
- 一時的なWebサーバー: ビルド後の
distディレクトリをホストする一時的なローカルWebサーバー(http-server)をNode.jsの子プロセスとして起動。 - 安定した環境: Puppeteerにサーバー経由(
http://localhost:8080)でアクセスさせることで、ブラウザと同一の安定したレンダリング環境を確保しました。
環境構築とサーバー起動のコード(抜粋)
// プロジェクト定数からドメインを取得し、未設定なら localhost:8080 を使用
const HOST_DOMAIN = SITE_URL || `http://localhost:${PORT}`;
const BUILD_ROOT = path.join(__dirname, 'dist');
const PORT = 8080;
// --- サーバーの起動ロジック ---
let serverProcess = exec(`npx http-server ${BUILD_ROOT} -p ${PORT} -s --silent`);
// サーバーが起動するまで待機
await new Promise((resolve) => setTimeout(resolve, 2000));
// --- Puppeteerのページアクセス ---
// Puppeteerは localhost:8080 にアクセスし、レンダリングを開始します
const serverUrl = `http://localhost:${PORT}/${REPORT_DIR}/${urlPath}`;
await page.goto(serverUrl, { waitUntil: 'networkidle0' });
2. 日本語フォントの適用
PDF生成を行うローカルサーバー環境には日本語フォントがないため、PDFに埋め込まれるフォントがおかしくなる問題が発生します。
- フォントの回避策: スクリプト内で、PDF生成直前にWebフォントの参照と適用スタイルをDOMに動的に挿入。これにより、日本語フォントでのPDF出力が可能に。 HTML側は速度を優先しWebフォントを使用していないため、このような動的挿入でPDFのみ適用されるようにしています。
フォント動的挿入のコード(抜粋)
// Webフォントの動的挿入ロジック (page.evaluateでブラウザ側で実行)
await page.evaluate((fontUrl) => {
// <link>タグを生成してDOMに挿入
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontUrl;
document.head.appendChild(link);
// @media print スタイルを強制的に挿入し、フォントを適用
const printStyle = document.createElement('style');
printStyle.textContent = `
@media print {
html, body {
font-family: 'Noto Sans JP', sans-serif !important;
}
}
`;
document.head.appendChild(printStyle);
}, WEBFONTS_URL);
🚨 最大の課題:PDF内リンクのURL参照問題
最も手こずったのは、PDF間のリンクでした。PDF内に埋め込まれたリンクが、デプロイ後もローカル開発環境のURL(http://localhost:8080)を参照してしまう問題です。 これは、ChromiumがHTMLをロードした際のベースURIをPDFリンクの基点として保持してしまうために起こります。
リンクの強制絶対パス化
これを解消するため、以下の手順でリンクをデプロイ後の完全な絶対パスに強制的に書き換えました。
- デプロイ先ドメインの利用: プロジェクトの定数(
SITE_URL)からデプロイ先のドメイン(例:https://example.com)を取得。 - 絶対URLの構築と置換: Node.jsでHTMLの内容を取得し、元のリンク(
/accessibility_report/top/)を、取得したドメインを基点とした完全なURL(例:https://example.com/accessibility_report/pdf/acr-top.pdf)に書き換えます。 - DOMへの再適用: 書き換え後のHTMLをChromiumに再適用(
page.setContent())することで、PDFに埋め込まれるリンクを意図したデプロイ後のドメインに確実に固定しました。
リンク書き換えのコード(抜粋)
// 1. 無効化したいリンク(.link-ignore-pdf)を物理的に削除
const ignoreLinkRegex = new RegExp(`(<a\\\\s+[^>]*class=["'][^"']*${ignoreClass}[^"']*["'][^>]*>)(.*?)(<\\/a\\\\s*>)`, 'gi');
content = content.replace(ignoreLinkRegex, '$2'); // <a>タグ全体を中身のテキストに置換
// 2. 詳細ページへのリンクを絶対URLに書き換え
const detailLinkRegex = new RegExp(`href="${reportDirRootLink}([^/]+)\\/"`, 'g');
const detailPdfUrl = `${pdfAbsoluteUrl}/acr-top.pdf`; // 例
content = content.replace(detailLinkRegex, (match, slug) => {
// リンクを <http://localhost>... ではなく、<https://example.com/>... に強制置換
return `href="${pdfAbsoluteUrl}/acr-${slug}.pdf"`;
});
// 3. 最終的なコンテンツをブラウザに再適用し、PDF出力へ
await page.setContent(content, {
waitUntil: 'domcontentloaded',
baseURL: baseUrlForContent
});
🎉 まとめ
今回はPDF間でリンクするという少し特殊な要件があったため手こずりましたが、Puppeteerの導入自体は割とすんなりでした。ビルドも早いですし、非常に便利なツールだと感じました!
今回の対応のポイントは以下です:
- ローカルサーバー経由でのレンダリングによる安定性確保
- 動的なWebフォント注入による日本語対応
- リンクの絶対パス化によるPDF間のリンク
PDF対応により、サービスの利便性は大きく向上しました。これからも、お客様にとって価値あるアクセシビリティ診断サービスの品質を追求していきます!
DTPからWebの世界へ飛び込み、気づけばマークアップもフロントエンドもディレクションもアクセシビリティもこなす"技の仙人"。リベロジック創業期からマルチに活躍し、今や社内の生き字引的存在。最近は「アクセシビリティ対応、もっとAIに頼れないかな?」と、プロンプトを駆使した効率化の探究にハマり中。技術も思考も、まだまだ進化中
フタさん
IAAP認定ウェブアクセシビリティスペシャリスト(WAS) / マークアップエンジニア / フロントエンドエンジニア / ウェブディレクター