この記事の結論
ホームページのPDF化は、動きの演出を止めて地図を画像に替えてから書き出すときれいに出ます。僕は自社サイト10ページをしおり付きの50ページ1冊にしました。PDF保存で白紙になる原因も書きます。
目次9項目
ホームページのPDF化で僕に必要だったのは、ブラウザの印刷ではなく、Claude CodeとPlaywrightで組んだ短いスクリプトでした。全ページを順に開いて動きの演出を止め、地図を画像に差し替えてからA4で書き出し、最後にpypdfでしおり付きの1冊にまとめています。
きっかけは取引先からの「会社概要をPDFで欲しい」という依頼で、2026年7月17日にこの仕組みを作りました。僕の会社のサイトurakata.bizの10ページを、ページ別のPDFと、しおり付きの1冊(50ページ・16MB)にしました。
この記事では、最初の方法が白紙になった理由、地図が消える問題の直し方、更新時の作り直しまで書きます。「ホームページをPDFで送ってと言われたけど、どう出すの?」と迷う方向けに、コードの要点も載せました。
ホームページのPDF化は「開く→演出を止める→地図を画像に→書き出す→結合」の5段
ホームページをPDF化する仕組みは、ページを開いて書き出すだけでは足りず、5つの段に分かれました。演出を止める段と地図を画像に替える段が、ほかの記事であまり語られない部分です。
対象は自社サイトの10ページです。トップ(index)、会社概要(corporate)、サービス、実績、メンバー、お知らせが前半です。後半は採用、問い合わせ、プライバシーポリシー、サイトマップで、この順に書き出しました。
できあがった納品物は3種類です。取引先に送ったのは会社概要のページ単体のPDFで、6ページ・1.4MBでした。全10ページを結合したしおり付きの1冊が50ページ・16MBで、ほかにページ別のPDFを01〜10の番号付きで残しています。
作業はClaude Codeに任せ、手順と罠は作業フォルダの記録(README)に残しました。途中で踏んだ罠は4つあります。先に全体を早見表にしておきます。
| 症状 | 原因 | 対処 |
|---|---|---|
| PDFがほぼ白紙になる | ふわっと出る演出(.fade-in)が動かないまま印刷された | CSSを注入して強制的に表示した状態にする |
| Googleマップの部分が空になる | 地図のiframeが印刷のときに描画されない | 表示中の地図をスクリーンショットして画像に差し替える |
| ヘッダー・フッター・問い合わせ欄が出ない | サイトのCSSが印刷時に隠す作り(仕様) | 不具合ではないので、そのままにした |
| Playwrightが起動しない | 同梱のChromiumとバージョンが合わない | インストール済みのChromeを指定して起動する |
僕なら、同じ依頼が来たら最初からこの表の対処を入れたスクリプトで始めます。1と2は、サイトの作りによってはどの会社でも起きる症状だからです。
ブラウザの印刷で「PDFに保存」する方法と、それでは足りなかった理由
ホームページを1ページだけPDF保存するなら、ブラウザの印刷機能がいちばん早い方法です。ただ今回は、10ページを1冊にまとめ、サイトを直すたびに作り直す前提だったので、手作業は選びませんでした。
Chromeの場合は、印刷したいページを開いてMacなら⌘+P、Windowsなら Ctrl+Pで印刷画面を出します。送信先でPDFへの保存を選べば、そのページがPDFになります。数ページで、作り直す予定がないならこれで十分です。
足りなかった理由は3つあります。1つ目は、10ページぶん同じ操作を繰り返し、最後に手で結合する手間です。2つ目は、ページの順番とファイル名をそろえたしおり付きの1冊を、毎回同じ形で作りたかったことです。
3つ目は、サイトを更新したら作り直しになる点です。会社概要は住所やメンバーが変わるたびに古くなります。コマンド1つで同じPDFが出てくる形にしておけば、作り直しに迷いません。
そこで最初に試したのが、画面を出さずに動かすheadless Chromeの--print-to-pdfでした。ここで1つ目の罠を踏みました。
公式のコマンドラインの説明では、--print-to-pdfは対象のページをoutput.pdfとして今のフォルダに保存するフラグです。
headless Chromeの--print-to-pdfだけだと、ほぼ白紙のPDFになった
--print-to-pdfでそのまま書き出すと、PDFはほぼ白紙でした。原因はサイトの演出で、スクロールして画面に入った要素をふわっと表示する仕組みが、書き出しのときには一度も動いていなかったのです。
僕の会社のサイトでは、.fade-inを付けた要素を最初は透明(opacity:0)にしています。画面に入ったことをIntersectionObserverで検知し、そこで初めて見える状態(opacity:1)に変えます。
人がブラウザでスクロールすれば順に表示されますが、headlessで開いてすぐ印刷すると、どの要素も画面に入ったことになりません。透明のまま印刷されるので、文字も写真もない紙ができあがります。
直し方は、印刷の前にCSSを足して、演出を最後の状態に固定することです。.fade-inは透明度を1、位置のずれをなし、切り替えのアニメーションもなしにしました。
同じスクリプトでは、見出しのマーカー線と英字ラベルの飾り線も、CSSで最終の大きさに固定しています。演出の途中の状態で印刷されないようにするための指定です。
なお公式の説明には、待つ時間の上限を決める--timeoutや、時間に関わる処理を早送りする--virtual-time-budgetもあります。ただ今回の白紙はスクロールが起きないことが原因なので、時間を延ばす方向の対処では直りません。
Googleマップのiframeは印刷に出ない、スクリーンショットに差し替えて直した
会社概要のページの地図は、印刷すると空白になりました。記録では、Googleマップの埋め込み(iframe)が印刷のときに描画されないと分かり、表示中の地図を撮って画像に差し替えてからPDFにしています。
最初は--virtual-time-budgetの値を伸ばして、地図の読み込みを待たせる方法を試しました。記録には、伸ばしても解決しなかったと残っています。地図が描かれる仕組みが、待ち時間では埋まらない種類のものだったと見ています。
そこでheadless Chromeのフラグだけで済ませるのをやめ、Playwrightに切り替えました。Playwrightとは、ブラウザをプログラムから操作するための道具です。
Playwrightなら、ページの中の要素を探し、撮影し、書き換えてからPDFにする順番を、1本のスクリプトで組めます。
差し替えの手順は3つです。地図のiframeを画面に入るまでスクロールし、3秒待ってからその部分だけをスクリーンショットします。撮った画像をdata URI(画像をそのまま文字列にした形)のimgタグにして、iframeと入れ替えます。
画像の高さは元のiframeの高さに合わせ、横幅は100%にしました。こうしておくと、PDFの上では地図が埋め込まれていた位置に、同じ大きさの静止画が入ります。地図を動かせなくなっても、紙の資料としては困りません。
今回は同じ処理を10ページに回す仕事なので、Node.jsのスクリプト1本にしました。Claude CodeからPlaywrightを動かす方法の違いは、playwright-cliとPlaywright MCPの違いに書いています。
ほかに踏んだ罠2つ:消えるヘッダーと、起動しないPlaywright
残りの2つは、直すものと直さないものに分かれました。ヘッダーが出ないのはサイトの仕様なのでそのままにし、Playwrightが起動しない問題だけを起動の指定で直しています。
ヘッダー・フッター・問い合わせ欄が消えるのは@media printの仕様だった
書き出したPDFには、サイトの上のメニューと下のフッター、問い合わせを促す欄が入っていませんでした。調べると、サイトのstyle.cssに、印刷のときだけこの3つを隠す指定がありました。
@media print {
.site-header, .site-footer, .cta-section { display: none }
}
紙で読むときに要らない部分を消す、印刷向けの作りです。公式ドキュメントでも、page.pdf()は印刷用のCSSでPDFを作ると説明されています。だからPDFにも、この指定が効きます。
画面と同じ見た目で出したいときは、page.pdf()の前にpage.emulateMedia()で画面用の表示に切り替える方法が公式に書かれています。僕は印刷向けの仕様どおりでよいと判断し、切り替えずに出しました。
同梱のChromiumが起動せず、インストール済みのChromeを指定した
もう1つは、Playwright本体の置き場所と起動の問題です。手元のMacには、Playwrightを単体では入れていませんでした。
ただ、@playwright/cliの中にPlaywrightが同梱されていたので、それを使っています。
使い方は、実行するときに環境変数NODE_PATHで、@playwright/cliの中のnode_modulesを指す形です。ところが同梱のChromiumはバージョンが合わず、起動できませんでした。
そこでchromium.launch({ channel: 'chrome' })と書き、Macに入っているChromeで動かしました。同梱のChromiumを使わない形にしたことで、起動の問題はここで片づきました。
site2pdf.jsの中身:1280×900で開き、800pxずつスクロールしてからA4で書き出す
スクリプトsite2pdf.jsの処理は、1ページごとに同じ流れを10回繰り返すだけです。開く、CSSを足す、スクロールする、待つ、地図を差し替える、PDFにする、の順になっています。
数字の決め方も残しておきます。画面の大きさは横1280×縦900で開き、読み込みはネットワークが落ち着くまで待ち、60秒で打ち切ります。スクロールは800pxずつ、150ミリ秒の間隔で下まで進め、先頭に戻してから4秒待ちます。
スクロールを入れたのは、画像の遅延読み込みを確実に始めさせるためです。演出はCSSで止めていますが、画像は画面に近づいて初めて読み込む作りのことがあります。最後の4秒は、地図のタイルなど残りの読み込みを実時間で待つためのものです。
要点を抜き出すと、次のようになります(保存先は一般的な書き方に直しています)。
const { chromium } = require('playwright');
const path = require('path');
const OUT_DIR = './pages';
const PAGES = ['index', 'corporate', 'service', 'works', 'member',
'news', 'recruit', 'contact', 'privacy', 'sitemap'];
const OVERRIDE_CSS = `
.fade-in { opacity: 1 !important; transform: none !important; transition: none !important; }
`;
(async () => {
const browser = await chromium.launch({ channel: 'chrome' });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
for (let i = 0; i < PAGES.length; i++) {
const num = String(i + 1).padStart(2, '0');
await page.goto(`https://urakata.biz/${PAGES[i]}.html`,
{ waitUntil: 'networkidle', timeout: 60000 });
await page.addStyleTag({ content: OVERRIDE_CSS });
// 800pxずつスクロールして遅延読み込みを始めさせ、先頭に戻す
await page.evaluate(async () => {
for (let y = 0; y < document.body.scrollHeight; y += 800) {
window.scrollTo(0, y);
await new Promise(r => setTimeout(r, 150));
}
window.scrollTo(0, 0);
});
await page.waitForTimeout(4000);
// 地図のiframeを撮影して画像に差し替える
for (const f of await page.$$('iframe[src*="google.com/maps"]')) {
await f.scrollIntoViewIfNeeded();
await page.waitForTimeout(3000);
const box = await f.boundingBox();
const shot = await f.screenshot();
await f.evaluate((el, { b64, h }) => {
const img = document.createElement('img');
img.src = 'data:image/png;base64,' + b64;
img.style.width = '100%';
img.style.height = h + 'px';
img.style.objectFit = 'cover';
el.replaceWith(img);
}, { b64: shot.toString('base64'), h: Math.round(box.height) });
}
await page.pdf({ path: path.join(OUT_DIR, `${num}-${PAGES[i]}.pdf`),
format: 'A4', printBackground: true });
}
await browser.close();
})();
page.pdf()の2つの指定には理由があります。公式ドキュメントでは用紙の既定がLetter、背景の印刷の既定がfalseです。A4で出し、背景色や帯の色を残すために、どちらも明示しました。
このコードを流用するなら、注意が2つあります。公式ドキュメントでは、networkidleでの待ち方はテスト用途では勧められていません。
また、地図の撮影に使った要素の持ち方(ElementHandle)も、今はLocatorを使う書き方が推奨されています。長く使うスクリプトにするなら、僕はこの2か所から書き換えます。
PDFにしたときの色の崩れは、別の作業でも踏んでいます。その件はChromeのPDFでグラデーションが黒くなる原因に書きました。
pypdfでしおり付きの1冊に結合し、サイト更新のたびに作り直す
ページ別のPDFは、Pythonのpypdfで1冊に結合しました。ページごとにしおりを付けたので、50ページあっても目次から目当てのページへ飛べます。サイトを更新したら、スクリプトを流し直して結合し直すだけです。
結合のコードそのものは作業フォルダに残しておらず、記録にあるのは「pypdfでしおり付きに結合」という手順だけです。公式ドキュメントで確かめた書き方で、同じことをする例を載せます。
# 例:pypdfの公式ドキュメントのappendの書き方に沿ったもの
from pypdf import PdfWriter
pages = [
("pages/01-index.pdf", "トップ"),
("pages/02-corporate.pdf", "会社概要"),
# ……10ページぶん並べる
]
writer = PdfWriter()
for path, title in pages:
writer.append(path, outline_item=title)
writer.write("site-all.pdf")
PdfWriter.appendのoutline_itemは、取り込んだファイルの先頭にしおりを作るための引数だと公式に説明されています。ページ別のファイルを番号順に足していけば、しおりの並びもサイトの順番と同じになります。
サイトを更新したときの作り直しは、2つの手順です。1つ目は、NODE_PATHで@playwright/cliのnode_modulesを指してnode site2pdf.jsを流します。
これでページ別のPDFが作り直されます。2つ目は、そのPDFをpypdfで結合し直します。
取引先に送ったのは会社概要の1ファイル(6ページ)だけで、全ページの1冊とページ別のPDFは作業フォルダに残しています。
ホームページ自体の作り方はホームページの作り方3通りを比較に書いています。動きのある見せ場を素のHTMLで足す方法は、21st.devの部品を素のHTMLに移す方法にまとめました。
まとめ
- ホームページを1ページだけPDF保存するなら、ブラウザの印刷(⌘+P/Ctrl+P)で足りる
- 全ページを1冊にして作り直す前提なら、Playwrightで開く→演出を止める→地図を画像に→A4で書き出す、をスクリプトにする
- headless Chromeの
--print-to-pdfだけでは、スクロールで出る演出が動かず、ほぼ白紙になることがある - 地図のiframeは印刷に出ないので、撮影して画像に差し替える。ヘッダーが消えるのは印刷用CSSの仕様のことが多い
- 僕なら、最初にサイトのCSSで
.fade-inのような演出と@media printの指定を確かめてから書き始めます
よくある質問
ホームページを1ページだけPDF保存するなら何が一番早いですか?
ブラウザの印刷機能です。ChromeならMacは⌘+P、WindowsはCtrl+Pで印刷画面を開き、送信先でPDFへの保存を選びます。ページ数が少なく、作り直す予定がないなら、スクリプトを書くよりこちらが早く済みます。
PDFにするとヘッダーやフッターが消えるのは不具合ですか?
多くはサイト側の印刷用CSSの仕様です。僕の会社のサイトは@media printでヘッダー・フッター・問い合わせ欄を消す作りでした。PlaywrightのPDF出力は印刷用のCSSで描かれるので、画面と同じ見た目が欲しいなら、書き出す前に画面用の表示へ切り替えます。
PDFの背景色や帯の色が出ないのはなぜですか?
背景を印刷しない設定が既定だからです。Playwrightのpage.pdf()はprintBackgroundの既定値がfalseなので、trueを指定すると背景色や背景画像が入ります。僕の会社のスクリプトでもtrueにしています。
全ページを結合したPDFにしおりを付けるにはどうしますか?
Pythonのpypdfで結合するときに、ページごとの見出しを渡します。PdfWriter.appendには、取り込んだファイルの先頭にしおりを作るためのoutline_item引数があります。ページ別のPDFを番号順に足していけば、目次から各ページへ飛べる1冊になります。