Each page is a <g> element with a data-page attribute. The value can be any string — OpenCRVS just uses the presence of the attribute, not its value, to detect pages.
Convention: use data-page="1", data-page="2", etc. to match the visual order.
The Transform Pattern
Here is the most important thing to understand about multi-page SVGs:
Every page's content must be designed in the coordinate space (0, 0) to (width, pageHeight) — not in the total SVG coordinate space.
But in the combined SVG, page 2 needs to sit visually below page 1. To achieve this, each page after the first gets a transform="translate(0, offset)" where offset = (pageNumber - 1) × pageHeight:
Page
transform
Page 1
(none)
Page 2
transform="translate(0, 842)"
Page 3
transform="translate(0, 1684)"
When OpenCRVS extracts each page for rendering, it removes the transform attribute. The content then renders at its native coordinates (y=0 to y=pageHeight), filling the PDF page correctly.
Visual result in the SVG file vs PDF output:
The transform is only there so design tools (Figma, Inkscape, Illustrator) display both pages stacked vertically in a single canvas. The transform plays no role in the final PDF output.
Step-by-Step: Creating a Multi-Page Template
1. Decide your page dimensions
Pick a page size. Standard is 595 × 842 (A4 portrait in points).
2. Set root SVG dimensions
3. Wrap page 1 content
Content for page 1 uses coordinates from y=0 to y=842. No transform needed.
4. Wrap page 2 content with a translate transform
Page 2 content also uses coordinates from y=0 to y=842 (as if it were its own page). Apply transform="translate(0, 842)" to shift it visually below page 1 in the combined SVG view.
5. Add Handlebars expressions normally
All template variables and helpers work exactly the same on every page. Access $declaration, $metadata, helpers, etc. just as you would on a single-page template.
Handlebars Variables Across Pages
All template variables are available on every page — there is no per-page scoping.
Variable
Available on page 2, 3, etc.?
$declaration
Yes
$metadata
Yes
$review
Yes
$references
Yes
All built-in helpers
Yes
Custom country helpers
Yes
Handlebars is compiled once for the full SVG, then the DOM is split into pages. Variables and helpers are resolved before the split.
Important Constraints
All visible content must be inside a [data-page] group
When OpenCRVS detects [data-page] elements, only those elements are rendered into PDF pages. Anything at the root level of the SVG (outside any [data-page] group) is excluded from the output.
Move <defs> inside each page group that uses them
Design tools like Figma export <defs> (clip paths, gradients, masks) at the root SVG level. When a page is extracted, its wrapper SVG only contains the page group — root-level <defs> are not copied over. If your page content references a <clipPath> or <mask> by ID, move the <defs> inside the page group:
Page height is calculated from SVG dimensions, not content
If your height attribute is not an exact multiple of your page count, the PDF page height will be a fraction. Always set height = pageHeight × pageCount.
No page-level layout control
OpenCRVS does not support page-break hints, overflow, or automatic content flow across pages. You control exactly what appears on each page by placing content inside the appropriate [data-page] group.
Complete Two-Page Example
This minimal example shows the full structure for a two-page A4 certificate.
What happens during rendering
Step
Result
OpenCRVS finds [data-page] elements
2 groups found
PDF page height calculated
1684 ÷ 2 = 842
Page 1 extracted
<svg width="595" height="1684" ...><g data-page="1">...</g></svg> → PDF page 1
The transform="translate(0, 842)" on page 2 is removed during extraction. Page 2's content (designed at y=0..842) renders correctly on the second PDF page.
PDF page height = SVG height ÷ page count
PDF page width = SVG width (unchanged)
<g data-page="1">
<!-- all content for page 1 goes here -->
</g>
<g data-page="2" transform="translate(0, 842)">
<!-- all content for page 2 goes here -->
</g>
<!-- This will NOT appear in the PDF: -->
<text x="100" y="100">This is outside any data-page group</text>
<g data-page="1">
<!-- This WILL appear: -->
<text x="100" y="100">Page 1 content</text>
</g>
<!-- Avoid: defs at root, referenced from inside the group -->
<defs>
<clipPath id="clip1">...</clipPath>
</defs>
<g data-page="1">
<g clip-path="url(#clip1)">...</g> <!-- may not render correctly -->
</g>
<!-- Prefer: defs inside the page group -->
<g data-page="1">
<defs>
<clipPath id="clip1">...</clipPath>
</defs>
<g clip-path="url(#clip1)">...</g> <!-- renders correctly -->
</g>