Liberogic 的無障礙診斷服務原本以 HTML 格式提供證書和報告,但為了滿足「希望以 PDF 格式提供證書」的需求,現在已可以發行 PDF 格式的證書和報告!
為實現此功能,我們在構建流程中建立了一個系統,在輸出 HTML 的同時自動生成 PDF。
🛠️ PDF 化的基礎:Puppeteer 和環境構築
作為 PDF 生成工具,我們採用了可以無頭操作 Chromium 瀏覽器的 Puppeteer。由於 HTML 原本已用 Astro 構築,Puppeteer 的導入相對順利。
1. 利用本機伺服器確保渲染環境
Puppeteer 利用 Chromium 的列印功能,但直接讀取本機 HTML 檔案(file://)會出現 版面配置破損的問題。
為了避免這個問題,我們構建了以下環境。
- 臨時網路伺服器:啟動臨時本地網路伺服器(
http-server)作為 Node.js 子程序,用於託管建置後的dist目錄。 - 穩定的環境:藉由讓 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)。 - 建構並替換絕對網址: 使用 Node.js 取得 HTML 內容,將原始連結(
/accessibility_report/top/)改寫為以取得的網域為基點的完整網址(例如: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 的導入本身進行得相當順利。建構速度也很快,是一個非常實用的工具!
本次解決方案的重點如下:
- 透過本機伺服器進行渲染,確保穩定性
- 透過動態網頁字型注入實現日文支援
- 透過連結絕對路徑化實現 PDF 間的連結
透過 PDF 支援,本服務的便利性大幅提升。我們將持續追求為客戶提供具價值的 accessibility 診斷服務品質!
從 DTP 跨足 Web 世界,轉眼間便掌握了標記語言、frontend 開發、專案指導,以及 accessibility 等各項技能——是名真正的「技術高人」。自 Liberogic 創立初期便展現多才多藝的能力,如今儼然成為公司內的活字典。最近正沉迷於利用 AI 提示詞探索「accessibility 對應能否更多依靠 AI?」的效率化研究。技術與思維都在不斷進化中。
Futa(二)
IAAP 認證網頁無障礙專家(WAS)/ 標記語言工程師 / Frontend 工程師 / 網頁總監