Define Your Email Content Using Pure Templates In ColdFusion
In general, when rendering content in a ColdFusion application, I find it best to define your view templates as "pure templates". Meaning, the rendering logic within the view template file is completely driven by inputs that are either passed into that template (as a module attribute) or made available to that template (as an include context). This keeps the view template devoid of data fetching and manipulation logic. An email template is, essentially, a view template that's rendered to the CFMail
tag instead of being rendered to the browser. As such, the same "pure template" principles should be applied.
I like to define my email templates to be consumed using the CFInclude
tag. Meaning, they're a ColdFusion page that consumes the parent page's context (not an isolated context). In an ideal world, I'd like to define my email templates using ColdFusion custom tags / modules. But, I don't love all the ceremony of passing around attributes and capturing thistag.generatedContent
. This is just a personal choice - it's not a "best practice".
The structure of my email templates all follow the same pattern: a number of CFParam
tags that help define the inputs followed by a CFOutput
tag that renders the email to the current output buffer (typically using my ColdFusion custom tag email DSL). The source of the output buffer is defined by the calling context; it might be the page output buffer, it might be a CFSaveContent
output buffer, it might be a CFMail
output buffer—the email template doesn't care, it just generates output.
To see this in action, let's create a very simple "Welcome" email for a new user. For the sake of organizational simplicity, I like all of my email inputs to be captured under a single structure: partial
:
<!---
We're using the CFParam tags to help document which inputs are required in this email
template. This won't be an exhaustive definition (optional arrays, for example, are a
hard datatype to parameterize); but, this technique will catch most use-cases.
--->
<cfparam name="partial.user.name" type="string" />
<cfparam name="partial.user.email" type="string" />
<cfparam name="partial.profileUrl" type="string" />
<!--- Assume that the email will be rendered to a content buffer. --->
<cfoutput>
<h1>
Welcome #encodeForHtml( partial.user.name )#!
</h1>
<p>
Thank you for signing up to experience our amazing service!
We've created an account for you using the login:
<<strong>#encodeForHtml( partial.user.email )#</strong>>.
</p>
<p>
<a href="#partial.profileUrl#">View your profile</a> →
</p>
</cfoutput>
As you can see, this email template makes no assumptions about how it is being used. Other than that it assumes the existence of a partial
structure and it renders output to the "page".
And, now that we have this "pure template" for our email rendering, we can render it in a variety of contexts. For example, when developing this particular workflow, we can render this email template directly to the page for debugging:
<cfscript>
// NOTE: In Lucee CFML, we can use `localmode` to ensure that any UNSCOPED variables
// created during the rendering of the include template (such as those in the CFLoop
// tag) are automatically scoped to the LOCAL scope of this function.
(function() localmode = "modern" {
// Configure the hard-coded inputs for the email template.
var partial = {
user: {
id: 1,
name: "Julia Stiles",
email: "julia.stiles@example.com"
},
profileUrl: "https://www.example.com/account/1/profile"
};
// Since this is just a TEST of the email, we can render the email content
// directly to the screen without capturing it in an intermediary buffer.
include "./templates/emails/welcome.cfm";
})();
</cfscript>
Since this is for development purposes, I can just hard-code the partial
variable and then CFInclude
the email template. And, when we run this ColdFusion page, we get the following output:
As you can see, the email template, driven by the one partial
structure, rendered perfectly to the browser output.
In a production workflow, we can render the same template. Only, instead of rendering directly to the page, we're going to render to a CFSaveContent
buffer and then use that buffer to execute a CFMail
tag. In the following code, the sendWelcomEmail()
method is meant to be representative of a production workflow:
<cfscript>
sendWelcomEmail( 1 );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// NOTE: In Lucee CFML, we can use `localmode` to ensure that any UNSCOPED variables
// created during the rendering of the include template (such as those in the CFLoop
// tag) are automatically scoped to the LOCAL scope of this function.
public void function sendWelcomEmail( required numeric userID )
localmode = "modern"
{
// Simulate gather actual live data.
var user = getUser( userID );
// Generate the inputs for the email template using live user data.
var partial = {
user: {
id: user.id,
name: user.name,
email: user.email
},
profileUrl: "https://www.example.com/account/#user.id#/profile"
};
// Even though we could render the CFInclude directly to the CFMail body, I like
// to create an intermediary buffer for the email template. This makes it easier
// to debug issues if something is going wrong.
savecontent variable = "local.emailBody" {
include "./templates/emails/welcome.cfm";
}
mail
to = user.email
from = "no-reply@example.com"
subject = "Welcome to our service"
type = "html"
server = "127.0.0.1:1025"
async = false
{
// Render interpolated email content to CFMail tag.
echo( emailBody );
}
}
// Mock method to get user data.
public struct function getUser() {
return({
id: 2,
name: "Patrick Verona",
email: "patrick.verona@example.com"
});
}
</cfscript>
This time, instead of hard-coding the partial
structure, I'm doing some (mock) data fetching for the given user and then constructing the partial
from the user data. Then, when I invoke my CFInclude
tag to execute the email template, I'm capture the email output into the variable, emailBody
; which I'm then using in my CFMail
tag.
And, when we run this ColdFusion page and check the Mailhog email client, we get the following output:
As you can see, the email template, which was captured by a CFSaveContent
buffer and then shipped via CFMail
has successfully landed in my Mailhog inbox.
By coding the email template such that it is completely driven by inputs and generates output to the contextual output buffer, we've created a lot of flexibility. This makes it simple to both develop emails with hard-coded data and to send emails in a production ColdFusion setting using database-driven inputs.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →