Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Randy Brown
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Randy Brown

Define Your Email Content Using Pure Templates In ColdFusion

By on
Tags:

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:
		&lt;<strong>#encodeForHtml( partial.user.email )#</strong>&gt;.
	</p>

	<p>
		<a href="#partial.profileUrl#">View your profile</a> &rarr;
	</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:

Email template being rendered to the browser.

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:

Mailhog email client showing that email template was rendered to CFMail which was delivered via SMTP.

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel