In this article, you'll get a look at the basic controls on pages for HTML print layouts and an overview of the layout libraries that Populi uses to produce documents from these layouts.
The HTML Layout page

Here are the various functions and controls on the page:
- The left pane shows the layout code and lets you edit it. The right pane provides a preview of the layout.
- Depending on the layout type (schedule, transcript, etc.), you'll have various options to load data from your Populi site into the layout.
- To update the preview after you change the code or any of the drop-down selections, click Reload Data.
- You can toggle between setting the layout to Draft or Active.
- If you change anything and want to keep it, make sure to click Save!
- After you've saved any changes, a Revision History will be available.
If you want to use images in your layout, scroll to the bottom of the page. You can upload images, and once uploaded, you can replace or delete them.
With all that out of the way, please remember that you should only edit the HTML and CSS for a print layout if you are confident with your coding skills. If you aren't, Populi Support is here to help. Don't hesitate to ask for any kind of help with your HTML layout!
HTML Layout Overview
Each HTML page layout is run through two layout libraries before either being previewed or turned into a pdf: PagedJS and Handlebars.
PagedJS is what we use to make the HTML document 'printable'—it handles things like pagination and making sure the document is the correct size for physical paper, as well as adding margins and things like that. Think of it as being like a word processor—it takes digital information and formats it for use in the physical world.
Handlebars is what we use for variable substitution. It is what allows us to dynamically create documents without having to manually edit them. With handlebars, code that looks like this:
The student's name is {{student_name.first}} {{student_name.last}}
is translated in the final document to each students first and last names, for example:
The student's name is John Doe
Each layout is effectively broken up into two sections—a <style> block that contains CSS (or styling) instructions, and a block within a handlebars tag where almost all the variable substitution happens. This is the {{#each this}} tag.
The {{#each this}} tag
In Handlebars, the {{#each}} syntax is how you can iterate through a list or array of data and this refers to the current document that we're rendering. For our purposes, almost all variable substitution must happen within the {{#each this}} tag so that you get the correct variable substitution for the document you are printing. If you are printing multiple copies of a document this might seem like it doesn't matter very much, but for printing, for example, course schedules for every student in an Academic Term, you want to make sure that the correct information is on each one. This means that if you want to use handlebars variables within CSS styles, you must create a separate {{#each this}} within the style tag at the top of the document:
<style>
{{#each this}}
.{{schedule.schedule_id}} {
--event_height: 45px;
--row_height: calc(var(--event_height) / 60);
--num_hours: {{CSS_data.hours}};
--num_minutes: {{CSS_data.minutes}};
}
.timeline {
display: grid;
grid-template-rows: repeat(var(--num_hours), var(--event_height));
}
.gridlines {
display: grid;
grid-area: gridlines;
grid-template-rows: repeat(var(--num_hours), var(--event_height));
grid-column: 1 / 5;
}
.day_label {
text-align: center;
height: var(--event_height);
text-decoration: underline;
}
.day_events {
display: grid;
grid-template-rows: repeat(var(--num_minutes), var(--row_height));
}
{{#each CSS_classes as |class|}}
.{{class.class}} {
grid-row-start: {{class.start}};
grid-row-end: {{class.end}};
}
{{/each}}
{{/each}}
</style>
{{#each this}}
Layout content
{{/each}}
In this example, taken from the student schedule, we're setting up CSS variables to allow the timeline to generate the correct number of hour and minute rows for the events we're displaying, and each schedule has a random id number generated when the schedule is printed so that we avoid conflicting rules for different course schedules during bulk prints.
Raw text output
By default, Handlebars will perform what is known as HTML escaping on special characters, like & or =. This ensures that they won't cause issues inside the HTML that the layout is made up of. There are cases, however, where you may need handlebars to generate valid HTML. The body of the letter template is one such example since it can contain line break characters and other formatting characters that would be escaped otherwise. In such instances, use three brackets to surround your handlebars variables instead of two: {{{letter.body}}}
Background Images vs Image Tags
In some cases, you may want to display an image behind your text. There are two ways to do this—an image tag with some CSS, and the CSS background image property. Use the CSS background image property if you want the image to cover the whole page, and use the special @page selector, which is what PagedJS suggests using. Layout images are the same across every instance of the layout being printed, so for background images you can avoid creating duplicate CSS rules by using the image url only from the first instance of the document variables like so:
{{{this.0.layout_images.layout.image_name}}}
So this is the whole document, the .0 represents the first element, which will always be present because you need to be printing at least one thing, and then the rest of it is that element's version of the layout images.
<style>
@page {
background-image: url( {{{this.0.layout_images.layout.image_name}}} );
background-size: cover;
}
</style>
This will stretch the image across each page separately without unnecessarily duplicating the CSS rule for every student that you're printing a document for. You will have to set the opacity or alpha of the image before using it, as there is no way to set the opacity of a CSS background image separately from the content of the element.
For situations where you want an image behind a specific element, like the header, you can use an image tag:
<div class="background_image"><img src="{{layout_images.global.logo}}" width="100px" height="100px"></div>
along with CSS like this for opacity and positioning:
.background_image {
z-index: -1;
position: absolute;
top: 0;
right: 3.75in;
opacity: 0.25;
}
The z-index at -1 puts it behind every other element, and opacity is a range from 0 (transparent) to 1 (fully visible)
Handlebars functions
While most of what we're doing with Handlebars is simple variable replacement, there are a few special cases of note. There is documentation for these, but I'll summarize a couple common ones here.
#each
When you're iterating through a list or array of data, the Handlebars {{#each}} function has a few special rules. You can create a temporary name for the individual elements of a list by using this syntax:
{{#each programs as |program|}}
{{program.name}}
{{/each}}
You'll start by indicating that you want to run through the list one element at a time by starting with {{#each . Next you'll indicate which list you want to use. In our case, programs, which is an array of programs each containing data organized in the same way. To refer to the individual element, you'll create an alias for it by surrounding it with vertical pipes: as |program|. Note that the alias doesn't have to be the singular form of the list variable. It would work just as well to say programs as |blah| and then refer the element variables as {{blah.name}} for example.
#if
You can surround a block of code with a {{#if variable}} tag to show it if the variable is present, and ignore it if it is not. For example:
{{#if program.honors}}
<div class="honors"><strong>Honors:</strong> {{program.honors}}</div>
{{/if}}
#if helpers
We've extended the #if functionality with several additions—you can use notZero in the #if block to handle string and number formatted values of 0.00 , which is useful for currency and GPA display. It looks like this:
{{#if (notZero variable_name)}}
{{/if}}
If you want to check against a value rather than variable existence, you can use valueCheck like so:
{{#if (valueCheck variable_name "value")}}
{{/if}}
If you want to check that a numeric value is within a range, use the inRangeCheck helper:
{{#if (inRangeCheck numeric_variable_name "1" "100")}}
{{/if}}
The order of the values matters—it's the variable, then the lowest part of the range, then the highest part. {{#if (inRangeCheck variable "low" "high")}}
Date Formatting
formatDate
We've also created our own handlebars function to help with date formatting. All dates are formatted as timestamps by default, and you can change their display values by running them through formatDate:
{{formatDate timestamp_variable_name "mdy"}}
should return the date formatted like this: 01/01/2026
If you want a fully custom formatted date, you'll have to use the format date function several times in a row, like this:
{{formatDate timestamp_variable_name "text_month"}} {{formatDate timestamp_variable_name "ordinal_day"}}, {{formatDate timestamp_variable_name "full_year"}}
should return the date formatted like this: January 1st, 2026
The complete list of date format values is as follows:
| passthrough value | timestamp equivalent | comment |
|---|---|---|
| 'mdy' | "MM/DD/YYYY" | 01/17/2026 |
| 'dmy' | "DD/MM/YYYY" | 17/01/2026 |
| 'ymd' | "YYYY/MM/DD" | 2026/01/17 |
| 'time' | "h:mma" | 1:04pm |
| 'day' | "D" |
1 day of the month with no leading zero for single digits |
| 'padded_day' | "DD" |
01 day of the month with leading zero for single digits |
| 'ordinal_day' | "Do" | ordinal day of the month—1st, 13th, etc |
| 'month' | "M" |
1 for January, 2 for February, etc |
| 'padded_month' | "MM" |
01 for January, etc |
| 'short_text_month' | "MMM" |
Jan for January, etc |
| 'text_month' | "MMMM" | January |
| 'short_year' | "YY" |
23 for 2026, etc |
| 'full_year' | "YYYY" | 2026 |
| 'padded_hour' | "HH" | padded hours in 24 hour format |
| 'padded_minute' | "mm" | padded minutes |
formatTextDate
You can also format the date as text by using the formatTextDate helper:
{{formatTextDate timestamp_variable_name "ordinal_day"}} day of {{formatDate timestamp_variable_name "month"}}, {{formatDate timestamp_variable_name "full_year"}}
will return the date formatted like this: First day of January, Two Thousand Twenty Five . This may be useful for creating dynamic Diplomas, for instance.
The complete list of text date format values is as follows:
| passthrough value | comment |
|---|---|
| 'day' |
seven,sixteen
|
| 'ordinal_day' |
seventh, sixteenth
|
| 'month' |
January, March, October
|
| 'decade' |
twenty-three, fifteen
|
| 'century' |
two thousand, nineteen hundred
Dates that aren't in the 1900s or the 2000s are not supported at this time. Please avoid graduating students in the distant past or the far future. New centuries will be added as they occur.
|
| 'full_year' |
nineteen hundred sixty-five, two thousand fifteen
|
Barcodes
The barcode helper is mainly useful for library or id cards, and will output a scannable barcode into your print layout. It supports most major barcode formats (CODE128, CODE39, EAN-2, EAN-5, EAN-8, EAN-13, UPC, ITF, MSI, Pharmacode, and Codabar). Here is a link (https://github.com/lindell/JsBarcode/wiki/Options) to all the options available. Only a single option is allowed at a time.
{{{barcode example_value "Barcode Type" options}}}
Here's an example from the ID card layouts:
<div class="barcode-cover">
{{{barcode card.person.populi_id "CODE39" marginLeft=0}}}
</div>
CSS Styling
One of the main ways you can adjust how your print layout displays with the HTML layouts is through the use of the CSS styling language. CSS is made of selectors that are added to HTML elements, and then you can attach styling or display rules to them inside the <style> element.
For example, take this short snippet of CSS and HTML, used in the header of several layouts:
<style>
.organization_info {
display: grid;
place-items: center;
}
.organization_address {
text-align: center;
}
.address {
display: block;
}
</style>
<div class="organization_info">
{{#if layout_images.global.logo}}
<div class="organization_logo">
<img src="{{layout_images.global.logo}}" width="100" height="100">
</div>
{{/if}}
<div class="organization_name"><h2>{{organization.name}}</h2></div>
<div class="organization_address">
<span class="address">{{organization.address.street}}</span>
<span class="address">{{organization.address.city}}, {{organization.address.state}} {{organization.address.postal}}</span>
</div>
<div class="organization_phone">{{organization.phone_number}}</div>
</div>
There is an html <div> element that contains all the other elements. It has the class organization_info. The way you reference a class in the CSS style area is to put a period in front of the name, like this: .organization_info . This lets you affect that <div> element and all of its contents. The reason we use classes is that multiple elements on a page can have the same class, and we can style them all at once.
Display
You can adjust the display of the elements inside a <div> by using the CSS display property. Here's a good article on what the display property does. For layout purposes, the display: grid and display: flex are both very useful. Grid can be used to position elements strictly, and Flex can be used for orderly positioning where the exact position doesn't matter as much.
Uploaded Fonts
You can upload fonts using the Layout Asset Manager (if you have the correct permissions).
To use a newly uploaded font, you'll need to set the @font-face property like so, and then set some css property to use the font name that you've assigned to the font url:
@>font-face {
font-family: "FontName";
src: url("{{{@root.assets.global.fonts.font_handle}}}");
}
.class_for_text_in_font {
font-family: "FontName";
}
Web Fonts
You can import web fonts from sources like Google Fonts. For example, if you wanted the Raleway font, you'd select the font weights you want to use on the font family page, and then go to the Selected Families area and select the @import style of importing a font. You'll get something like this:
<style>
@import url('https://fonts.googleapis.com/CSS2?family=Raleway:wght@100&display=swap');
</style>
You'll take the @import line and paste it inside the <style> tags of your print layout. Then, when you want to use the font, you'll simple add a font-family CSS property to the CSS selector you want to use the font for. If you selected multiple font weights you can also specify that here:
.text-selector-example {
font-family: 'Raleway', sans-serif;
font-weight: 100;
}
The font family property has the name of the font family, and then a fallback in case the font fails to load for any reason. In this case, it'll load as a basic sans-serif font if the Raleway font fails to load. You can also use these generic font types (listed in the font-family link above) to style your text, without requesting a specific font.
Font Sizing
For consistency between the preview and the final pdf, use font sizes defined in px, rather than pt—not all browsers handle pt the same as our pdf converter does. Font sizes defined in pt will still work, but spacing in the final pdf may vary from what you see in the preview.
Capitalization
Many variables (like the date formatting outputs) will be all lower case by default so that you can capitalize however you please using the text-transform property.
0 Comments