Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Mark Drew
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Mark Drew@markdrew )

Using Function LocalMode To Render Templates During Static Site Generation In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion

The other day, I took a look at using the Function localmode to more-safely render templates in Lucee. The reason that localmode makes rendering templates more safe is that any unscoped variable assignment, performed during the CFML template evaluation, is applied to the local scope, not to the variables scope. This behavior prevents unexpected variable-leakage, which can sometimes lead to race-conditions and cross-request contamination in a multi-threaded application. In my previous post, I mentioned that I was using this localmode feature during the generation of static HTML files. As such, in this follow-up post, I wanted to take a quick look at how I was generating the static HTML site in Lucee 5.3.2.77.

The HTML site that I needed to generate was extremely simple. It was just a list-detail relationship based on a database query. As such, any off-the-shelf solution (like Jekyll or Gatsby) would have been 1,000-times overkill. To generate my static site, all I needed was the data and a couple-of-hundred lines of ColdFusion code.

The basis for the static site generation was a function called, renderTemplateWithContext(). This function - which executes with localmode="modern" - takes a CFML template and a "context" Struct, and then evaluates the template into an output buffer.

<cfscript>

	/**
	* I render the given template using the given context variables.
	* 
	* @template I am the template to render.
	* @rc I am the context that exposes variables during the template rendering.
	*/
	public string function renderTemplateWithContext(
		required string template,
		required struct rc
		)
			// NOTE: By using the MODERN localmode, any unscoped variable assignment
			// performed during the template rendering will be saved to the LOCAL SCOPE
			// of this function (not to the VARIABLES scope of this page).
			localmode = "modern"
			output = false
		{

		// Prepare the View content.
		savecontent variable = "local.body" {

			include "./templates/#template#";

		}

		// Wrap the View content in the Layout content.
		savecontent variable = "local.layout" {

			include "./templates/layout.cfm";

		}

		return( layout );

	}

</cfscript>

Because the context Struct is an argument of the function, it is implicitly available to the CFML template which is executing as part of the renderTemplateWithContext() invocation. As such, our dynamic CFML templates can reference the rc "scope" for any data required during rendering.

ASIDE: Calling the context variable, rc, is a nod to FrameworkOne, a light-weight ColdFusion application framework.

Once rendered, the resultant output buffer can then be written to the local file-system as flat .htm files.

To explore this static site generation in ColdFusion, I'm going to create a list-detail relationship for some of my favorite Movies. Before we look at the rendering engine, let's look at the CFML templates. Here is the list of movies:

<cfoutput>

	<h1>
		Movies
	</h1>

	<ul>
		<cfloop index="movie" array="#rc.movies#">
			<li>
				<a href="./movies/#encodeForHtmlAttribute( movie.slug )#">
					#encodeForHtml( movie.name )#
				</a>
			</li>
		</cfloop>
	</ul>
	
</cfoutput>

As you can see this CFML template expects certain values to be available on the rc scope. Namely, rc.movies. The template also declares the unscoped variable, index="movie", for the list iteration. Since this template is going to be evaluated inside of a function operation with localmode="modern", we can safely assume that the movie variable will not leak out of the rendering context.

The other CFML templates in the static site are equally simple. Here's the movie detail template:

<cfoutput>

	<h1>
		#encodeForHtml( rc.movie.name )#
	</h1>

	<p>
		&laquo; <a href="#rc.baseFolder#index.htm">Back to Movies</a>
	</p>

	<p>
		Tags:
		<cfloop index="tag" array="#rc.movie.tags#">
			
			<a href="#rc.baseFolder#tags/#encodeForHtmlAttribute( tag.slug )#"
				>#encodeForHtml( tag.name )#</a>

		</cfloop>
	</p>

	<p>
		#encodeForHtml( rc.movie.summary )#
	</p>

	<p>
		Released: #encodeForHtml( rc.movie.releasedAt )#
	</p>
	
</cfoutput>

And, here's the Tag detail page that lists the movies associated with a given tag:

<cfoutput>

	<h1>
		#encodeForHtml( rc.tag.name )#
	</h1>

	<p>
		&laquo; <a href="#rc.baseFolder#index.htm">Back to Movies</a>
	</p>

	<ul>
		<cfloop index="movie" array="#rc.tag.movies#">
			<li>
				<a href="#rc.baseFolder#movies/#encodeForHtmlAttribute( movie.slug )#">
					#encodeForHtml( movie.name )#
				</a>
			</li>
		</cfloop>
	</ul>
	
</cfoutput>

As you can see, each of these CFML templates expects certain rc-scoped variables to exist; and, haphazardly (yet safely) declares unscoped variables during the template evaluation.

None of these CFML templates represent a full HTML file on its own. If you refer back to the renderTemplateWithContext() function above, you'll see that each template rendering actually uses two templates: the provided one and a "layout" template. The layout template is just another CFML template that wraps the results of the "view template". Here's the layout template for this demo:

<cfoutput>
	
	<!doctype html>
	<html lang="en">	
		<head>
			<meta charset="utf-8" />
			<title>
				#encodeForHtml( rc.title )#
			</title>
			<link rel="stylesheet" type="text/css" href="#rc.baseFolder#css/main.css"></link>
		</head>
		<body>

			<!--- This is the result of the view-template. --->
			#body#

		</body>
	</html>

</cfoutput>

Now that we see how simple the CFML templates are in this demo, let's take a look at the static site generation logic. Again, the focus of this post is the renderTemplateWithContext() function, which is operating in localmode="modern" such that it safely captures any unscoped variable assignment performed within the CFML templates. The rest of the build logic just glues the renderTemplateWithContext() calls to the right template with the expected "context" data:

NOTE: I've tried to organize this file so that it can be read top-to-bottom.

<cfscript>
	
	(() => {

		stopwatch variable = "local.timer" {

			var data = new DataReader().parseJson( fileRead( "./data.json" ) );

			// Define the directory into which we will be building the static site.
			var exportPath = "./export/";

			buildExports( exportPath );
			buildIndex( exportPath, data );
			buildMovies( exportPath, data );
			buildTags( exportPath, data );
			archiveExports( exportPath );

		} // END: Stopwatch.

		dump( "Static site generated in #numberFormat( timer, ',' )# ms" );
		dump(
			label = "VARIABLES Scope",
			var = variables,
			showUdfs = false
		);

	})();

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I render the given template using the given context variables.
	* 
	* @template I am the template to render.
	* @rc I am the context that exposes variables during the template rendering.
	*/
	public string function renderTemplateWithContext(
		required string template,
		required struct rc
		)
			// NOTE: By using the MODERN localmode, any unscoped variable assignment
			// performed during the template rendering will be saved to the LOCAL SCOPE
			// of this function (not to the VARIABLES scope of this page).
			localmode = "modern"
			output = false
		{

		// Prepare the View content.
		savecontent variable = "local.body" {

			include "./templates/#template#";

		}

		// Wrap the View content in the Layout content.
		savecontent variable = "local.layout" {

			include "./templates/layout.cfm";

		}

		return( layout );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I create the base exports folder along with the necessary sub-folders and assets.
	* 
	* @exportPath I am the file-location of the exports folder.
	*/
	public void function buildExports( required string exportPath ) {

		// If the folder exists from a previous build, remove it - start over fresh.
		if ( directoryExists( exportPath ) ) {

			directoryDelete(
				path = exportPath,
				recurse = true
			);

		}

		directoryCopy(
			source = "./assets",
			destination = exportPath,
			recurse = true
		);

		directoryCreate(
			path = ( exportPath & "movies" ),
			createPath = true
		);

		directoryCreate(
			path = ( exportPath & "tags" ),
			createPath = true
		);

	}


	/**
	* I create the index file for the site (which lists the movies).
	* 
	* @exportPath I am the file-location of the exports folder.
	* @data I am the data for the static site.
	*/
	public void function buildIndex(
		required string exportPath,
		required struct data
		) {

		// I contain the variables needed for the template rendering.
		var context = {
			baseFolder: "./",
			title: "Movies",
			movies: data.movies
		};

		fileWrite(
			file = ( exportPath & "index.htm" ),
			data = renderTemplateWithContext( "index.cfm", context )
		);		

	}


	/**
	* I create the individual movie detail pages.
	* 
	* @exportPath I am the file-location of the exports folder.
	* @data I am the data for the static site.
	*/
	public void function buildMovies(
		required string exportPath,
		required struct data
		) {

		for ( var movie in data.movies ) {

			// I contain the variables needed for the template rendering.
			var context = {
				baseFolder: "../",
				title: movie.name,
				movie: movie
			};

			fileWrite(
				file = ( exportPath & "movies/" & movie.slug ),
				data = renderTemplateWithContext( "movie.cfm", context )
			);

		}

	}


	/**
	* I create the individual tag detail pages.
	* 
	* @exportPath I am the file-location of the exports folder.
	* @data I am the data for the static site.
	*/
	public void function buildTags(
		required string exportPath,
		required struct data
		) {

		for ( var tag in data.tags ) {

			// I contain the variables needed for the template rendering.
			var context = {
				baseFolder: "../",
				title: tag.name,
				tag: tag
			};

			fileWrite(
				file = ( exportPath & "tags/" & tag.slug ),
				data = renderTemplateWithContext( "tag.cfm", context )
			);

		}

	}


	/**
	* I archive the exports folder as a ZIP file.
	* 
	* @exportPath I am the file-location of the exports folder.
	*/
	public void function archiveExports( required string exportPath ) {

		var zipPath = ( exportPath.left( -1 ) & ".zip" );

		compress( "zip", exportPath, zipPath, false );

	}

</cfscript>

As you can see, each one of the "glue" methods - buildIndex(), buildMovies(), and buildTags() - does nothing more than locate the necessary data, invoke the "render" method, and then write the resultant output to an HTML file. And, when we run this, we're presented with an ./exports folder that can be viewed directly from the user's local file-system:

User navigating through static site generated with Lucee CFML 5.3.2.77.

At the end of the static site generation build process, I also dump out the variables scope to demonstrate that no unscoped variables escaped outside of the CFML template rendering function:

None of the unscoped variable assignments leaked into the Variables scope in Lucee 5.3.2.77.

The localmode feature of Lucee CFML provides a nice way to dynamically render CFML templates without polluting the variables scope of the execution context. In this case, such a feature can be used to quickly and cleanly generate a static site from a bag of data and a set of ColdFusion files.



Reader Comments

This is partly why I try not to use:

cfinclude

Inside functions, especially, if there are nested includes. It's quite difficult to assess how many variables need scoping.

However, using:

localMode="modern"

Is an excellent way to resolve this issue! No more having to use the 'varscoper' tool:)

Reply to this Comment

Just out of interest, do you know when:

localMode="modern"

Was introduced to ColdFusion, or is this just a Lucee only feature? This is a perfect solution for the many legacy websites, I will, no doubt, have to trawl through, in the near future!

Reply to this Comment

@Charles,

I believe it is Lucee only. I had never heard of it in the Adobe CF world. Though, to be fair, I didn't know it was in Lucee either until recently :P

But, yes - agreed, using cfinclude inside a Function is problematic because a number of unexpected things can happen that are not obvious unless you've stumbled over them before.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.