Apex

VisualforceのPDFで改ページを制御する書き方

取引先の概要を1ページ目、ぶら下がる取引先責任者を2ページ目以降に出力するPDFを作ります。@pageのCSSとpage-break-beforeでページを分け、ページ番号を数える方法もあわせて示します。

2023.09.27

なぜページを区切る必要があるのか

Visualforceのページを丸ごとPDFにすると、内容が長いレコードほど1ページに詰め込まれ、読みにくくなります。取引先の概要と、ぶら下がる取引先責任者の一覧を別々のページに分けて出力できれば、印刷しても見やすいPDFになります。ページを分ける仕組みはCSSのpage-break-beforeと、用紙サイズを決める@pageのふたつです。公式ドキュメントは、PDFレンダリングサービスが解釈できるCSSの範囲を「CSS 2.1相当まで」と明記しています。page-break-beforeはCSS 2.1に含まれる標準的なプロパティなので、そのまま使えます。

取引先と取引先責任者のレコードを用意する

まず、PDFに出す元データを作ります。

  1. 取引先レコードを作成する

    アプリケーションランチャーから「取引先」を開き、新規作成します。取引先名は任意の値(例:00001)で構いません。

  2. 取引先責任者レコードを複数作成する

    作成した取引先の関連リストから、取引先責任者を3件作成します。姓は00001-0001、00001-0002、00001-0003のように連番にしておくと、後の動作確認で区別しやすくなります。

Apexクラスで取引先責任者を1回のSOQLで取得する

標準コントローラの拡張クラスを作り、取引先責任者を1回のSOQLでまとめて取得します。

public with sharing class StandardControllerPDFSample {
    public Account acc { get; set; }
    public List<Contact> conList { get; set; }
    public Integer totalPage { get; set; }

    public StandardControllerPDFSample(ApexPages.StandardController stdCtrl) {
        this.acc = (Account) stdCtrl.getRecord();
        Id accId = this.acc.Id;
        this.conList = [SELECT Id, LastName FROM Contact WHERE AccountId = :accId LIMIT 3];
        this.totalPage = this.conList.size() + 1;
    }
}

標準コントローラ拡張。取引先責任者を1回のSOQLで取得します。読み取りだけの処理です。

SOQLは文字列連結で組み立ててDatabase.query()で実行することもできますが、ここではインラインSOQLとバインド変数で書いています。取引先Idはユーザー入力ではないので危険度は高くありませんが、バインド変数で書くほうが短く、意図も伝わります。件数をLIMIT 3で固定しているのはサンプルとしての制約です。実運用では、関連レコードが多い取引先だと、PDFに変換する前のレスポンスサイズが15MB未満という上限に近づくことがあるため、必要な件数だけを絞り込む設計にしてください。

Visualforceページでページを区切る

renderAs="pdf"でPDFに変換し、@pageで用紙サイズを、page-break-beforeでページの区切りを指定します。

<apex:page standardController="Account" extensions="StandardControllerPDFSample"
           renderAs="pdf" applyHtmlTag="false" applyBodyTag="false" showHeader="false">
<head>
<style>
@page {
    size: A4;
}
</style>
</head>
<body>
<apex:variable value="{!1}" var="pageNumber"/>
<div>
<h1>取引先</h1>
<table>
<tr><td>Account Id : {!acc.Id}</td></tr>
<tr><td>page ( {!pageNumber} / {!totalPage} )</td></tr>
</table>
</div>
<apex:variable value="{!pageNumber+1}" var="pageNumber"/>
<apex:repeat value="{!conList}" var="con">
<div style="page-break-before:always;"></div>
<table>
<tr><td>Contact Id : {!con.Id}</td></tr>
<tr><td>Contact LastName : {!con.LastName}</td></tr>
<tr><td>page ( {!pageNumber} / {!totalPage} )</td></tr>
</table>
<apex:variable value="{!pageNumber+1}" var="pageNumber"/>
</apex:repeat>
</body>
</apex:page>

apex:pageのrenderAs="pdf"でPDF出力にします。@pageとpage-break-beforeでページを分けます。

applyHtmlTagとapplyBodyTagをfalseにしているのは、<head>と<body>を自分で書くためです。showHeaderはLightning Experienceとモバイルアプリでは常にfalseに上書きされると公式ドキュメントに明記されていますが、他の呼び出し経路でも確実にヘッダーを消すため、明示的に指定しておきます。ページ番号はapex:variableを使って、出力するたびに1つずつ増やしています。区切り用のdivはapex:outputLabelで包まず、素のdivのまま置きます。apex:outputLabelは本来フォーム項目のラベル用のコンポーネントです。ページ番号の行も、<tr>の直下に文字列を置くと構造が崩れるため、<td>で囲んでいます。

Apexから直接PDFを生成する場合

アクションからページを開く方法のほかに、ApexからPageReference.getContentAsPDF()を呼んでPDFのバイナリを直接取得する方法もあります。メール添付やContentVersionへの保存に使う場合はこちらが向いています。getContentAsPDF()は、呼び出すapex:page側のrenderAsの値に関係なく、常にPDFとして内容を返す点がgetContent()との違いです。ただし、外部サーバーのリソースを参照しているVisualforceページに対しては、Apexトリガーの中からgetContentAsPDF()を呼ぶと例外になります。このページは外部リソースを使っていないため影響しませんが、画像を外部URLで読み込むページでPDFを作る場合は注意してください。

PDFを開くアクションを配置する

取引先の詳細画面からPDFを開けるように、カスタムアクションを用意します。

  1. クイックアクションを新規作成する

    オブジェクトマネージャの「取引先」からボタン、リンク、アクションを開き、新規アクションを作成します。Visualforceページを呼び出す設定を選びます。

  2. 呼び出すVisualforceページを選択する

    作成したVisualforceページを指定し、ラベルをわかりやすい名前(例:PDF改ページサンプル)にして保存します。

  3. ページレイアウトのアクション欄に配置する

    取引先のページレイアウトを編集し、作成したアクションをアクション欄にドラッグして配置します。

アクションの詳細画面。種別はカスタムVisualforce、呼び出すページと高さが指定されている
作成したアクションの詳細です。アクション種別がカスタムVisualforceで、呼び出すページが指定されていることを確かめます。
取引先レコードの画面。右上のアクション欄に配置したボタンが赤枠で示されている
取引先レコードの画面です。赤枠のボタンが、配置したアクションです。ここを押すとPDFが別タブで開きます。

動作確認で見るポイント

取引先レコードで作成したアクションを押すと、PDFが新しいタブで開きます。今回の設定なら合計4ページになります。

ページ内容
1ページ目取引先のAccount Idとページ番号1/4
2ページ目1件目の取引先責任者のContact IdとLastName、ページ番号2/4
3ページ目2件目の取引先責任者の情報とページ番号3/4
4ページ目3件目の取引先責任者の情報とページ番号4/4

各ページの先頭で改ページされていること、ページ番号が1つずつ増えていることを確認します。

PDFの1ページ目。取引先のIDとページ番号1/4だけが印字されている
1ページ目です。取引先の情報とページ番号が1/4になっていることを見てください。
PDFの2ページ目。1件目の取引先責任者のIDと姓、ページ番号2/4
2ページ目です。1件目の取引先責任者がページの先頭から始まり、番号が2/4に進んでいます。
PDFの3ページ目。2件目の取引先責任者のIDと姓、ページ番号3/4
3ページ目です。姓の連番と、ページ番号が1つずつ増えていることを見比べてください。
PDFの4ページ目。3件目の取引先責任者のIDと姓、ページ番号4/4
最後の4ページ目です。取引先責任者3件が1ページずつに分かれ、合計4ページで終わります。

ここで間違えやすい

page-break-beforeを<table>や<tr>に直接指定すると、レンダラーによっては無視されます。区切り専用の空のdivを間に挟んで、そこにpage-break-beforeを指定してください。
間違い何が起きるか
@pageのプロパティ名を打ち間違える用紙サイズの指定が効かず、既定のサイズで出力される
ページ番号用のapex:variableを更新し忘れる全ページで同じ番号が表示される
関連レコードを絞り込まずにサブクエリを組むPDFに変換する前のレスポンスサイズが15MB未満という上限に近づく
showHeaderをtrueのままにするLightning Experience以外の呼び出し経路でタブヘッダーが出てしまうことがある

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、renderAs="pdf"の仕様とapex:pageの属性を確認しています

まとめ

  • Visualforceのページを1ページ目と2ページ目以降に分けるには、@pageで用紙サイズを、page-break-beforeでページの区切りを指定します
  • page-breakはtableやtrに直接指定せず、区切り用の空のdivに指定してください
  • ページ番号はapex:variableを出力のたびに更新して数えます
  • SOQLは文字列連結ではなくバインド変数で書くほうが安全で短くなります
  • PageReference.getContentAsPDF()はapex:pageのrenderAsの値に関係なく常にPDFを返しますが、外部リソースを参照するページをApexトリガーの中から呼ぶと例外になります
  • showHeaderはLightning Experienceとモバイルアプリでは常にfalseに上書きされます

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。