Templating - Single Entry Templates
- 1 - Introduction to ExpressionEngine
- 2 - How ExpressionEngine Thinks About Content
- 3 - Planning the Content Model
- 4 - Installation & Basic EE Setup
- 5 - Building Our Content Model - Directories, Categories & Fields
- 6 - Building Our Content Model - Channels, Adding Content & Publish Layouts
- 7 - Templating Intro - Templates, Template Groups & Staying DRY
- 8 - Templating - Single Entry Templates
- 9 - Templating - Multi-entry Templates
- 10 - Final Touches - Contact Form, SEO Metadata, Error Handling & Performance Optimization
Remember back in our planning chapter when we touched on the difference between Single Entry and Multi-entry content types. Our Content Model has two Channels that hold multiple entries (Services that holds the main Services landing page plus several sub-pages and Work that holds all of our portfolio entries). Still, the Homepage, About page, and Contact page are all examples of Single Entry content, and this is a better place to start actually building.
Before we dive in, though, I highly suggest taking the time to read through EE’s own documentation on the Template Language and while you’re there, note that there is very extensive documentation on EE Templates - make sure to utilize the documentation as you’re learning.
In This Article:
Step 1: Building Our Homepage
We already have a working template for our Homepage, our site/index
template, but so far the only
content is hard-coded, so we want to update that to pull in the fields we built out, assigned to the Homepage
Channel, and then populated in earlier chapters.
Displaying ‘Native’ Content with the Channel Entries Tag
We retrieve content stored inside our Channels using EE’s most powerful tag: the Channel Entries Tag. At a high level, this tag can either output a single Entry or loop through multiple Entries in a Channel (or multiple Channels if needed) and outputs whatever content you want. The tag can accept certain parameters to further refine what Entries get included in the loop, but its most basic usage is very simple.
- Replace the hard-coded
<h1>
insite/index
with the following, save, then refresh your browser:{exp:channel:entries} <h1>{title}</h1> {/exp:channel:entries}
The result is that the tag loops through literally all of our Entries (from all Channels) and outputs everything between the opening and closing tags for each Entry in the loop, so in this case, we get one
<h1>
tag populated with EE’s default{title}
‘single variable’ output to the page.
- Now try adding the
channel
andlimit
parameters to the openingexp:channel:entries
tag:{exp:channel:entries channel="homepage" limit="1"} <h1>{title}</h1> {/exp:channel:entries}
The result here is what we’re looking for - we just want to display one Entry from our Homepage Channel. Technically we don’t need the
limit
here because our Homepage Channel is limited to 1 Entry, but it’s a good habit to get into for Single Entry templates.
- Next, we want to output our Page Heading and Page Intro fields:
- The Page Intro field is a Rich Text fieldtype, so the content it outputs
already has HTML included in it, so we can literally just output the
{page_intro}
tag right after our<h1>
:{exp:channel:entries channel="homepage" limit="1"} <h1>{title}</h1> {page_intro} {/exp:channel:entries}
- The Page Heading, however, we had intended to use as a way for the user to specify the
page’s main heading, but by default, we want to load EE’s default Title like we’re already
doing and only override that if the Page Heading field is populated. The way we do this is
by using Conditional
Tags. It’s definitely worth reading the documentation here too, because there’s a ton of
stuff you can do with Conditional Tags. I’ll try to incorporate as many examples as possible, but
seriously, RTFM. The most basic conditional is the
{if}
tag:{if condition} data to show if condition is met {/if}
which can also be used with
{if:else}
and{if:elseif}
:{if condition_1} Condition 1 is true {if:elseif condition_2} Condition 1 is false, but Condition 2 is true {if:else} Neither condition is true {/if}
So inside our
<h1>
tag, we will check to see if the Page Heading exists and if so, display that, otherwise, output the default EE Title:{exp:channel:entries channel="homepage" limit="1"} <h1> {if page_heading} {page_heading} {if:else} {title} {/if} </h1> {page_intro} {/exp:channel:entries}
NOTE: We don’t need a conditional around our Page Intro field - because this field is Rich Text, we don’t need any surrounding HTML because the HTML is part of the field output based on what the user enters into the editor. If nothing is added to the Page Intro field, the
{page_intro}
variable tag will just not display anything at all.
- The Page Intro field is a Rich Text fieldtype, so the content it outputs
already has HTML included in it, so we can literally just output the
- Finally, we want to pull in our Hero Carousel field. Grid and File
Grid fields use Variable
Pairs - remember, our Hero Carousel can have multiple rows and each of those can
have multiple columns (or sub-fields). Just like the
{exp:channel:entries}
tag has an opening and closing tag to loop through all the Entries, Grid and File Grid fields have opening and closing tag and loop through all the rows of content.NOTE: Again, like the
{exp:channel:entries}
tag, opening tags for Grid and File Grid fields can also accept parameters to filter the output - it’s definitely worth keeping in mind the documentation for Fieldtypes as you’ll use this a lot.The opening and closing tags use the ‘short name’ of the main field (in our case
hero_carousel
), and then in each row, or iteration through the loop, we have access to the column (or sub-field) data via a tag that combines the main field short name and the column short name like this:{grid_field:column_field}
.Note: From now on, I’ll deliberately omit large chunks of code and only focus on the parts I’m discussing to limit the length of code in the tutorial.
Go ahead and add this to your template, above our
<h1>
but still inside the openingexp:channel:entries
tag, then save and refresh your browser:<div class="carousel"> {hero_carousel} <div div class="slide"> <h2>{hero_carousel:heading}</h2> <p>{hero_carousel:text}</p> <p>{hero_carousel:file}</p> </div> {/hero_carousel} </div>
There are a few things to note in the output here:
- We’ll need more output to actually turn this into a carousel - for now, we’re just outputting each row from our Hero Carousel field in a new <div>
- The first column in a File Grid is always has a ‘short name’ of
{file}
and is always ‘required,’ but the other two columns (Heading and Text) are not required in this case, so we want to add conditionals to handle that - Accessing any File Field (including the first column of a File Grid) has several ways
you can interact with it. You can access it via a single variable like we’ve done above, which outputs
the public path to that file (e.g.,
example.com/images/uploads/hero_banners/slide1.jpg
). But you can also use a variable pair and access certain info from the file separately - check out the documentation on the File Fieldtype. - It’s possible that there might not be any rows at all in our Hero Carousel field, and
if that’s the case, we don’t even want to output the surrounding <div> with class
"carousel." Grid and File Grid fields have individual variables we can
use to conditionally output these opening and closing tags, specifically the
{count}
(i.e., which row we’re in) and{total_rows}
(i.e., how many total rows are being returned).
So to clean this up and finish this off, try this instead of above:
{hero_carousel} {if hero_carousel:count == 1} <div class="carousel"> {/if} <div class="slide"> {if hero_carousel:heading}<h2>{hero_carousel:heading}</h2>{/if} {if hero_carousel:text}<p>{hero_carousel:text}</p>{/if} <p>{hero_carousel:file wrap="image"}</p> </div> {if hero_carousel:count == hero_carousel:total_rows} </div> {/if} {/hero_carousel}
Now we’re opening the ‘carousel’
<div>
in the first row of the loop and closing it on the last, but for all rows in the loop, we output the ‘slide’ <div> and are checking if the Heading and Text is populated before we output it and we’ve used the handywrap
parameter on the File field that outputs our image!
The last step is here is to actually get this looking like a carousel - to do this, we’ll be using the Bootstrap Carousel with
Captions. Just like before, we’ll still only output the starting HTML the first time through the loop and
the closing HTML in the final time through, so the only part that gets output for each row is the
‘carousel-item’ <div>
.
A couple more things to note:
- We only want the first item to have the ‘active’ class, so we can use the same conditional we used before again to do this
- The HTML for the image needs classes, so we can’t use the
wrap
parameter any more - instead, we’ll update this to use the ‘variable pair’ so we can access the{url}
and{title}
separately - Also regarding the image, we want to use the Manipulation we setup to make sure all the images are
the same size:
- If you are using the single variable, the syntax to set which Manipulation to use is like
{file_field:manipulation}
or in this specific case{hero_carousel:file:large}
- Using the variable pair, you add the Manipulation identifier to the single variables within
the pair, eg:
{file_field} <img src="{url:manipulation}" alt="{title}" width="{height:manipulation}" height="{height:manipulation}" /> {/file_field}
- If you are using the single variable, the syntax to set which Manipulation to use is like
So to finish this off, putting all of this together and updating our Hero Carousel code, our final code in our template should be like this:
{hero_carousel}
{if hero_carousel:count == 1}
<div id="carouselExampleCaptions" class="carousel slide" data-ride="carousel">
<div class="carousel-inner">
{/if}
<div class="carousel-item {if hero_carousel:count == 1}active{/if}">
{hero_carousel:file}
<img src="{url:large}" class="d-block w-100" alt="{title}">
{/hero_carousel:file}
<div class="carousel-caption d-none d-md-block">
{if hero_carousel:heading}<h2>{hero_carousel:heading}</h2>{/if}
{if hero_carousel:text}<p>{hero_carousel:text}</p>{/if}
</div>
</div>
{if hero_carousel:count == hero_carousel:total_rows}
</div>
<a class="carousel-control-prev" href="#carouselExampleCaptions" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleCaptions" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
{/if}
{/hero_carousel}
NOTE: In this example, we’re using the variable pair for the ‘file’ column of our Hero Carousel and separately outputting the
{url:large}
variable (which is the URL to the large image Manipulation) and for thealt
parameter we’re using the{title}
variable, but note this is not the same{title}
from our Channel Entry, it’s just one of the Template Tags available to the File fieldtype and outputs the Title metadata assigned to the file when it was first uploaded.
I’ve added some basic styling to the stylesheet already, so this is looking good, and we can move on!
Displaying ‘Visiting’ Content
Our Homepage isn’t done yet though - we also want to showcase our latest 3 portfolio projects, so we need to set up
our Homepage template (site/index
) to pull in this visiting content. To do this, we’ll use another
Channel Entries loop, just with different parameters set. In this case, to pull in ‘visiting’ content, we will:
- Set the
channel="work"
parameter - Limit the tag to only loop through 3 Entries with the
limit="3"
parameter - Also include the
dynamic="no"
parameter. By default, the EE Channel Entries tag sets some parameters dynamically based on what’s in the URL, so specifyingdynamic="no"
tells the Channel Entries Tag to completely ignore the URL, which we want to do in this case. (Read more about thedynamic
parameter here) - Specify how the tag should order and sort the entries - by default, EE orders by date and sorts descending, so technically, these parameters aren’t necessary, but it’s worth adding them to highlight you could change it. See options for ordering and options for sorting.
- Go ahead and add links to the Work Entry detail pages even though we know the links won’t work
yet. The URL structure we want is like this:
example.com/work/sample-project
where ‘sample-project’ is the URL Title of each Work Entry, so we will use EE’s Path Variables to do this.
<h2>Latest Projects</h2>
<div class="row">
{exp:channel:entries channel="work" limit="3" dynamic="no" orderby="date" sort="desc"}
<div class="col-4">
<h3><a href="{path='work/{url_title}'}">{title}</a></h3>
<p><em>{entry_date format="%F %d %Y"}</em></p>
{work_images limit="1"}
<img src="{work_images:file}" alt="{work_images:caption}" />
{/work_images}
{if client_name}
<h3>{client_name}</h3>
{/if}
<p>{excerpt}</p>
<p><a href="{path='work/{url_title}'}">Read More ></a></p>
</div>
{/exp:channel:entries}
</div>
There are a couple of things to note here too:
- You can’t nest Channel Entries Tags directly in the same template, so add the above code after the
closing
{/exp:channel:entries}
tag we had before. If you ever needed to do that, you’d need to use an Embedded Template - just be careful doing this as loops inside loops could quickly get out of hand and adversely affect performance - We’re adding formatting to the Entry Date output:
{entry_date format="%F %d %Y"}
- EE offers extensive Date Variable Formatting options we can utilize to get the date output looking how we want it
So now we have our Homepage not only outputting the page-specific content we added but also looping through the first 3 Work entries and displaying limited info for each one with links to the full ‘detail’ page, which we’ll be building out later.
Step 2: Building the Contact Page
Now that we’ve run through an example of a Single Entry template using the Channel Entries Tag in detail
(excruciating detail?!), building out the next couple of templates won’t take anywhere near as long. Let’s start
with the Contact page. We want our Contact page to live at example.com/contact
, so we need to set up
another Template Group, this time named "contact" and then the contact/index
template for our
contact page:
- In the CP, go to
Developer > Templates
, then use the blueNew
button next to the Template Groups heading in the left sidebar.NOTE: Clicking
Create New Template
here will add a new Template to our existingsite
group, which is not what we want. - Enter the name "contact", leave everything else as the default and save
- In your text editor, open up the
contact/index
template and set the Template Layout we’re going use with{layout="layouts/_wrapper"}
on the very first line (just like we did for our Homepage)
- Now lets re-use the first Channel Entries Tag from our Homepage template (ie
site/index
) with the exception of the{hero_carousel}
variable pair - because the Contact Channel doesn’t have that field available to it. We also need to update thechannel
parameter to look for Entries in the Contact Channel:{exp:channel:entries channel="contact" limit="1"} <h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1> {page_intro} {/exp:channel:entries}
- Save the template and go to
example.com/contact
in your browser
That wasn’t so bad! But we’re not quite done yet - the Contact Channel has a few new fields we haven’t worked with in Templates yet, so lets add those:
- First we’ll output our Address field - this is a Grid, so we use the variable pair
again. In this case, we know there can only ever be 1 row, so I’m putting the heading
inside the loop - that way if there isn’t a row at all, the heading won’t show. We also need to be
aware that we left the Line 2 column optional so we need to cater for that:
{address} <h2>Address</h2> <p> {address:line_1} {if address:line_2}<br />{address:line_2}{/if} <br />{address:city}, {address:state} {address:zip_code} </p> {/address}
- Our Departments field is very similar, though this time we know there could be multiple
rows, so we need to use a conditional so we only output the heading the first time through the
loop. We made all the columns required, so this one should be simple:
{departments} {if departments:count == 1}<h2>Departments</h2>{/if} <p> <strong>{departments:name}</strong> <a href="tel:+1-{departments:phone}">{departments:phone}</a> <a href="mailto:{departments:email}">{departments:email}</a> </p> {/departments}
- The Map Embed Code is a Textarea field intended to just paste in a Google Map
<iframe>
. We could theoretically wrap it with HTML to make it responsive, in which case we’d use a conditional to make sure it existed first, so if it didn’t, we wouldn’t have empty HTML tags, but I’m not going to bother, and we can just slap{map_embed_code}
into our{exp:channel:entries}
loop by itself.
So our final contact/index
template should look like this:
{layout="layouts/_wrapper"}
{exp:channel:entries channel="contact" limit="1"}
<h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1>
{page_intro}
<div class="row align-items-center">
<div class="col-md-6">
{address}
<h2>Address</h2>
<p>
{address:line_1}
{if address:line_2}<br />{address:line_2}{/if}
<br />{address:city}, {address:state} {address:zip_code}
</p>
{/address}
{departments}
{if departments:count == 1}<h2>Departments</h2>{/if}
<p>
<strong>{departments:name}</strong>
<a href="tel:+1-{departments:phone}">{departments:phone}</a>
<a href="mailto:{departments:email}">{departments:email}</a>
</p>
{/departments}
</div>
<div class="col-md-6">
{map_embed_code}
</div>
</div>
{/exp:channel:entries}
Step 3 - Building the About Page
Our last Single Entry template is our About page, and again, we’ll need a new Template Group so we can use the
about/index
template to achieve our example.com/about
URL:
-
In the CP, go to
Developer > Templates
, then use the blueNew
button next to the Template Groups heading in the left sidebar
-
This time, let’s select ‘contact’ under the Duplicate existing group? section - this will
duplicate all the templates inside the group and save us a little time
-
Open
about/index
with your text editor, update thechannel
parameter to useabout
and strip out the fields specific to the Contact channel (our Bootstrap row with the Address, Departments and Map Embed Code):{layout="layouts/_wrapper"} {exp:channel:entries channel="about" limit="1"} <h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1> {page_intro} {/exp:channel:entries}
-
Now we have to do the same thing we did for the Contact template and add the new fields we haven’t seen
yet… so here is where we build out the templates for our Page Builder Fluid
Field. This will be a long sub-step, but stick with me!
Remember, a Fluid Field is a single field we can assign to a Channel that lets the content editor add multiple other fields to the page - in whatever order they want - and however many they want. In previous chapters, we planned out the fields available to our Page Builder and then set up those fields (and the Page Builder field itself) in the CP.
You can read up on the Template Tags for a Fluid Field here, but the syntax is kind of similar to a Grid field in that you have your main field variable pair, and inside that, you have your ‘sub-fields’ referenced with another variable pair with the concatenated field and sub-field short names, like this:
{fluid_field} {fluid_field:sub_field} ... {/fluid_field:sub_field} {/fluid_field}
But the difference with a Fluid Field is that any content inside the sub-field is referenced with the special
{content}
tag - which can be either a single variable or a variable pair depending on the sub-field fieldtype. Any code inside this inner{fluid_field:sub_field}
variable pair will render each time that sub-field is used within Fluid Field, regardless of whether the{content}
tag loops or not.Page Builder: Centered Heading Sub-field
So in our example, the Centered Heading field is just a Text Input field, so would normally just need a single variable, so in our case, we can output the data within the Page Builder like this:
{page_builder} {page_builder:ff_centered_heading} {content} {/page_builder:ff_centered_heading} {/page_builder}
Of course, this just outputs the content with no styling, so we need to add HTML around it. Remember, all code inside the
{page_builder:ff_centered_heading}
tag pair renders for each use. In this case, we’ll just wrap a<h2>
with a Bootstrap class to center it, and we’re done:{page_builder} {page_builder:ff_centered_heading} <h2 class="text-center">{content}</h2> {/page_builder:ff_centered_heading} {/page_builder}
Page Builder: Rich Text Sub-field
Our Rich Text field, however, is a Grid, which normally uses a variable pair to get to the columns of the Grid, so our
{content}
tag needs to be used as yet another variable pair and anything inside this pair is looped through for as many rows as there are in the Grid:{page_builder} {page_builder:ff_rich_text} {content} ... {/content} {/page_builder:ff_rich_text} {/page_builder}
Then inside the
{content}
variable pair, you reference the individual fields with{content:field_short_name}
, so for our Rich Text field, try starting with this and refresh to see how it looks:{page_builder} {page_builder:ff_rich_text} {content} {content:text}<br /> {content:width}<br /> {content:background}<br /> {content:center_content_container} {/content} {/page_builder:ff_rich_text} {/page_builder}
Of course, this needs fleshing out - the Width, Background, and Center Content Container? fields aren’t intended to be displayed to the page but rather used in the HTML to adjust the display. Width is a Select Dropdown and Background is a Radio Button Group and in both cases, the content we have access to is the ‘value’ part of the Value/Label pairs we set up when creating these fields. We strategically set up both of those with actual class names. Center Content Container? is a Toggle field, which is a Boolean - either on (1) or off (0), so we can use a conditional to check that and add a class accordingly:
{page_builder} {page_builder:ff_rich_text} {content} <div class="row {if content:center_content_container == 1}justify-content-center{/if}"> <div class="{content:width} {content:background}"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text} {/page_builder}
IMPORTANT!
So now we’ve seen two examples of how to build sub-field template code inside a Fluid Field, but in both examples, I showed the main
{page_builder}
variable pair with just the individual sub-field variable pair inside of it to make it easier to follow. To actually let the Page Builder field render all of the sub-fields as the content editor mixes and matches them, we have to have all sub-field variable pairs present inside one main{page_builder}
pair. In other words, each of the sub-fields available to our Fluid Field should have a corresponding variable pair inside the main{page_builder}
pair. So combining the two we’ve done so far, we get this:{page_builder} {page_builder:ff_centered_heading} <h2 class="text-center">{content}</h2> {/page_builder:ff_centered_heading} {page_builder:ff_rich_text} {content} <div class="row {if content:center_content_container == 1}justify-content-center{/if}"> <div class="{content:width} {content:background}"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text} {/page_builder}
And of course, this must be inside our About page template’s Channel Entries tag, so our
about/index
template at this point should look like this{layout="layouts/_wrapper"} {exp:channel:entries channel="about" limit="1"} <h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1> {page_intro} {page_builder} {page_builder:ff_centered_heading} <h2 class="text-center">{content}</h2> {/page_builder:ff_centered_heading} {page_builder:ff_rich_text} {content} <div class="row {if content:center_content_container == 1}justify-content-center{/if}"> <div class="{content:width} {content:background}"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text} {/page_builder} {/exp:channel:entries}
Page Builder: Document Downloads Sub-field
For the Document Downloads sub-field, this a File Grid, so it will look pretty similar to our last example, but this time, it could have multiple rows within the grid. I’ll also take the opportunity to showcase what we can do with a File field when using its variable pair (docs here).
NOTE: As we look to add the template code for more sub-fields available to our Page Builder, remember the code will also go inside the main
{page_builder}
pair, but for the sake of space, I’ll just be showing the sub-field variable pair.So for this sub-field, the template code to add to our
{page_builder}
field is:{page_builder:ff_document_downloads} <h2>Document Downloads</h2> {content} <p> {content:file} <a href="{url}" target="_blank">{content:title} ({extension}, {file_size:human})</a><br /> {/content:file} {content:summary} </p> {/content} {/page_builder:ff_document_downloads}
Things to note:
- The code outside of the
{content}
variable pair only gets rendered once for each use of the sub-field inside the Page Builder - so the<h2>
will only display once even if there are 30 files - unless the content editor adds the Document Downloads filed to the Page Builder multiple times. - Note the use of the ‘file’ variable pair - I did that so we could output the extension and filesize,
which uses the handy
:human
modifier per the docs.Page Builder: Rich Text & Image Sub-field
This one will be pretty similar to our regular Rich Text sub-field - this time we used a File Grid because each row contains an image. Remember we get access to the file in a File Grid using the
file
short name, and just like in other Fluid Field sub-fields, we reference that from within the{contact}
variable pair and using the two-part concatenated tag, i.e.:{content:file}
.We’re also going to check the content of our Image Position dropdown and add a Bootstrap class to flip the order if the users want the image on the right.
{page_builder:ff_rich_text_image} {content} <div class="row align-items-center {if content:image_position == 'right'}flex-md-row-reverse{/if}"> <div class="col-md-6"> <img src="{content:file:resize width='540'}" alt="{content:image_caption}" /> </div> <div class="col-md-6"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text_image}
Page Builder: Featured Work Sub-field
We’re almost there - this is the last sub-field! This sub-field uses a Relationship field which allows the content editor to ‘relate to’ - or pull in - specific Entries from our Work Channel. But remember, we’ve already built this functionality out in our Homepage template, just there, we did it through a Channel Entries Tag. We can re-use that code and just adjust it so that instead of looping through an
{exp:channel:entries}
loop, we loop through the selections in the Relationship field. Here’s the documentation on Relationships, but the syntax differs just a little big when referencing a Relationship from inside a Fluid Field (see here).So here we will re-use the second
{exp:channel:entries}
tag from our Homepage template (site/index
), but make the following changes:-
Firstly, it all has to go within our
{page_builder:ff_featured_work}
sub-field variable pair - remember anything inside this field gets rendered in each place the editor adds our Featured Work sub-field to the Page Builder -
Previously, we used the
{exp:channel:entries}
tags to loop through the latest 3 entries. This time, the entries included in the loop depend on the Relationship field, so each selection is looped through in the{content}
variable pair. So we just replace the{exp:channel:entries}
opening and closing tags with{content}
and{/content}
-
Because the individual fields for each Work Entry in the loop are now inside a
{content}
variable pair, we have to reference them accordingly using{content:field_name}
, so we have to go through and add thecontent:
part to each of our individual fields before they’ll output
So here is our final code for this sub-field:
{page_builder:ff_featured_work} <div class="row"> {content} <div class="col-4"> <h2><a href="{path='work/{content:url_title}'}">{content:title}</a></h2> <p><em>{content:entry_date format="%F %d %Y"}</em></p> {content:work_images limit="1"} <img src="{work_images:file}" alt="{work_images:caption}" /> {/content:work_images} {if content:client_name} <h3>{content:client_name}</h3> {/if} <p><a href="{path='work/{content:url_title}'}">Read More ></a></p> </div> {/content} </div> {/page_builder:ff_featured_work}
Putting Our Page Builder All Together
Remember, we must have each of the individual sub-field variable pairs inside our main
{page_builder}
variable pair, so our final code for our Page Builder Fluid Field looks like this:{page_builder} {page_builder:ff_centered_heading} <h2 class="text-center">{content}</h2> {/page_builder:ff_centered_heading} {page_builder:ff_rich_text} {content} <div class="row {if content:center_content_container == 1}justify-content-center{/if}"> <div class="{content:width} {content:background}"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text} {page_builder:ff_document_downloads} <h2>Document Downloads</h2> {content} <p> {content:file} <a class="button" href="{url}" target="_blank">{content:title} ({extension}, {file_size:human})</a><br /> {/content:file} {content:summary} </p> {/content} {/page_builder:ff_document_downloads} {page_builder:ff_rich_text_image} {content} <div class="row align-items-center {if content:image_position == 'right'}flex-md-row-reverse{/if}"> <div class="col-md-6"> <img src="{content:file}" alt="{content:image_caption}" /> </div> <div class="col-md-6"> {content:text} </div> </div> {/content} {/page_builder:ff_rich_text_image} {page_builder:ff_featured_work} <div class="row"> {content} <div class="col-4"> <h2><a href="{path='work/{content:url_title}'}">{content:title}</a></h2> <p><em>{content:entry_date format="%F %d %Y"}</em></p> {content:work_images limit="1"} <img src="{work_images:file}" alt="{work_images:caption}" /> {/content:work_images} {if content:client_name} <h3>{content:client_name}</h3> {/if} <p><a href="{path='work/{content:url_title}'}">Read More ></a></p> </div> {/content} </div> {/page_builder:ff_featured_work} {/page_builder}
- The code outside of the
-
The last thing to do in our About page Template is actually in preparation for staying DRY in other
templates. Right now, the Page Builder code is exclusively in our
about/index
template, but remember we’re also using the Page Builder in our Services Channel, which will use a different template. So that we can easily re-use this whole field, let’s put the whole thing (i.e., the code block above) into a Template Partial:-
Go to
Developer > Templates
then go toTemplate Partials
in the left sidebar -
Click
Create New
then add the namepar_page_builder_fluid_field
and cut and paste in the whole{page_builder}
variable pair from ourabout/index
template, then save -
Replace that whole variable pair in the
about/index
template with just our partial:{par_page_builder_fluid_field}
Now our About page template looks much simpler, and we can re-use our Page_Builder field code. This is our entire
about/index
template now:<p>{layout="layouts/_wrapper"}</p> <p>{exp:channel:entries channel="about" limit="1"} <h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1> {page_intro} {par_page_builder_fluid_field} {/exp:channel:entries}</p> </pre>
-
Go to
What’s Next?
This was a long chapter, but we can re-use a lot of it in the next chapter. Now we have all of our Single Entry templates done, the next job to tackle is building out our Multi-entry templates - our Services section and our Work section.