Select Page

Adding turbo-cfml To My ColdFusion + Hotwire Demos Project

Ben Nadel
Published: April 26, 2024

Out of the box, Hotwire Turbo doesn’t work with ColdFusion / CFML because it doesn’t recognize .cfm as a valid HTML file extension. To “fix this”, I forked the Turbo project and created turbo-cfml, which does nothing but add cfm|cfml|cfc to the library’s regular expression pattern matching. This morning, I then added a “hello world” demo, using turbo-cfml to my ColdFusion + Basecamp Hotwire Demos project.

In previous demos, I had to rely on the the path_info behavior of the server; and, route all ColdFusion requests through one root hotwire.cfm template, using the path info to fake an .html page. For example, my “about” page was located at:


This URL will make a request to the ColdFusion template, hotwire.cfm, and pass /about.html as the cgi.path_info. This URL architecture tricks the Turbo library into thinking that this URL is pointing to an HTML file (not to a CFML file).

In my ColdFusion framework component, I was then overriding the request processing in my onRequest() event handler:

component {
	* I process the requested script.
	public void function onRequest( required string scriptName ) {
		if ( cgi.path_info.len() ) {
			var turboScriptName = cgi.path_info
				// Replace the ".htm" file-extension with ".cfm".
				.reReplaceNoCase( "\.html?$", ".cfm" )
				// Strip off the leading slash.
				.right( -1 )
			include "./#turboScriptName#";
		} else {
			include scriptName;

With this logic in place, a request to the ColdFusion server for:


… ends up executing the script:


This is viable; but, it’s a total pain in the bum! And, raises the barrier to entry for anyone building ColdFusion websites. Which is why I forked the Turbo library to add CFML file extensions.

Aside: Lucee CFMLL will call the onRequestStart() and onRequest() event handlers even if there’s no physical CFML template. As such, my demos project doesn’t actually have a hotwire.cfm template anywhere – I’m just using that template name as a hook into the request processing.

With that said, I wanted to add a “hello world” demo to my demos project using my new turbo-cfml library. This way, I’d have a copy-paste basis for future demos.

Getting this to work took a bit of trial and error, stemming from the fact that I’m hosting turbo-cfml on GitHub, not on It turns out, in order to install dependencies from GitHub, the installation container needs to have git installed.

I didn’t notice this in my prior exploration because I was running npm install directly on my host computer, which always has git installed. But, my ColdFusion + Hotwire project is running inside Docker, including the npm install. So, when I went to install turbo-cfml, I was getting the following error:

1016 verbose cwd /app/turbo-cfml
1017 verbose Linux 5.15.49-linuxkit
1018 verbose node v22.0.0
1019 verbose npm  v10.5.1
1020 error code ENOENT
1021 error syscall spawn git
1022 error path git
1023 error errno -2
1024 error enoent An unknown git error occurred
1025 error enoent This is related to npm not being able to find a file.

To me, this error meant nothing. But, I pasted it into ChatGPT; which was able to tell me that git needed to be made available. So, I updated my Dockerfile to include git and a more recent version of nodejs (which I believe was causing a separate installation issue for morphdom):

FROM ortussolutions/commandbox
RUN curl -fsSL | bash - \
	&& apt-get install -y \
		git \
		nodejs \

I then created a demo with the following package.json dependencies (truncated code):

	"name": "demo",
	"dependencies": {
		"@hotwired/stimulus": "3.2.2",
		"@parcel/transformer-less": "2.12.0",
		"parcel": "2.12.0",
		"turbo-cfml": "github:bennadel/turbo-cfml#cfml-8.0.4-2"

At first, npm was telling me that the version #cfml-8.0.4-2 didn’t exist. I believe this was a confusion on my part. I had thought that providing a tag name would work. But, it seems that only a branch name will work. So, I created a branch with the same name (cfml-8.0.4-2) and pushed it up to my turbo-cfml repository. At that point, the installation was successful.

Aside: I think some of my confusion came from the fact that the package-lock.json file was hiding some issues in my previous exploration.

With those development assets in place, I then created a small ColdFusion site with three pages. And, to the point of this post, the navigation links for this site all use plain .cfm URLs:

<h1> Turbo CFML Testing </h1>
	<a href="">Home</a> |
	<a href="./about.cfm">About</a> |
	<a href="./contact.cfm">Contact</a>

Now, when I click around in this ColdFusion site using turbo-cfml instead of @hotwired/turbo, the Turbo framework happily navigates across CFML pages:

User clicking around a ColdFusion website. The network activity shows fetch operations triggered for each navigation.

As you can see, I can navigate across the ColdFusion pages. And, by looking at the network activity, we can tell that these navigation events are being handled by Hotwire Turbo because:

  1. They are being requested via the fetch API.

  2. The initiator of the network request is turbo-cfml.

At this point, I now have a proven mechanism for using Hotwire Turbo in ColdFusion without any URL rewriting shenanigans.

Want to use code from this post?
Check out the license.