Closing The 404 Gap For Dynamic Page Generation With onMissingTemplate()
ColdFusion's new onMissingTemplate() function is a great addition to our toolbox, and mind blowingly useful for Dynamic Page Generation, but (as Ben Nadel notes) the problem of requiring a ColdFusion template to be requested in order to invoke the functionality has yet to be addressed.
In the most recent issue of the Fusion Authority Quarterly Update, Michael Dinowitz covers the onMissingTemplate functionality of Application.cfc -- new in ColdFusion 8 -- and how you can use it for Dynamic Page Generation, so for the most part I will not be covering that in this article. The FAQU article was terrific, hopefully opening the eyes of developers everywhere to the dynamic possibilities over and above error trapping (just like onMissingMethod). I wish I could link directly to the article, but you're going to have to buy the magazine. It's worth every penny, and tax deductible, so what are you waiting for?
I've got a working solution to this problem, and I thought I would share it with you in the interest of making it better. If you have ideas for improvement, please leave them in the comments.
A client of mine that provides a service to thousands of brick-and-mortar shops asked me to come up with a solution that would allow them to provide an informational page, possibly up to two or three (think in the terms of a very small company website) for each of their shops as part of their website. Pages like myclient.com/foo/ and myclient.com/bar/.
Sure, I could get dirty with the CFDIRECTORY and CFFILE tags, creating the foo and bar directories as needed; but like I said, there are thousands of clients. Trying to manage thousands of folders like this would quickly become cumbersome, not to mention the ridiculous amount of code duplication. What if a change needed to be made to one of the contained CFM files? Would you want to be the one to have to copy and paste the new file in, or write a script to copy it to all of the new folders? What if thousands of customers became hundreds of thousands? Could the file system handle that? Even if it could, think about how inefficient looping over the query returned from CFDIRECTORY would be.
Instead, what I decided to do was use onMissingTemplate() to dynamically generate these pages at run-time, from data stored in my client's database.
Essentially, a request comes in for myclient.com/foo/index.cfm, the file isn't found, and the error is handled by onMissingTemplate(). In this function, I simply check that the requested URL is in the format I'm expecting -- specifically that it has a single sub-folder from the root, and wants some .cfm file inside that folder. If this is the case, and the shop-site requested is defined in the database, and matches some business rules, I cfinclude my template that handles the shop-site request. Otherwise, log the error and return false; which runs my site-wide 404 handler.
<cffunction name="onMissingTemplate" returnType="boolean" output="true">
<cfargument name="targetpage" required="true" type="string">
<cfset var shop = listGetAt(arguments.targetPage, 1, "/") />
<cfif listLen(arguments.targetPage) eq 2
and right(arguments.targetPage, 4) is ".cfm">
<!--- apply business rules as necessary --->
<cfset request.shop = shop />
<cfinclude template="/shop/DynamicPageGenerator.cfm" />
<cfreturn true />
<cfreturn false />
If you would like more information on using onMissingTemplate for Dynamic Page Generation, I suggest you pick up a copy of the most recent edition of the FAQU.
My shop-site handler include parses the requested URL to determine which shop it needs to pull information for, and displays it. If the request was for index.cfm, I can display the content for the main page. If the request was for hours.cfm, I can display the content for the store hours page. To keep my dynamic page generation template clean, I create these page templates as further includes, which set up the page template for each page type. Don't think of them as files being requested, so much as messages being sent. "I need the store hours for foo."
On page 2 I'll show what problem this introduces, and the way that I resolve it with some simple ColdFusion code.
The Problem That This Introduces
This is all well and good, but sadly, doesn't work if the user or link doesn't specify index.cfm or another .cfm template inside our faux directory, and instead just stops with the folder name like so: myclient.com/foo/. In this case, ColdFusion is never given the opportunity to handle the request, and instead the web server software -- in my case, IIS -- does. What we need to do is find a way to have IIS hand the request off to CF for handling
My Solution For The New Problem
This is easily accomplished with custom error pages. (The same can be done for Apache and any other server software worth its salt.)
Since the folder the user is requesting (/foo) doesn't actually exist, IIS throws a 404 error. However, if it did exist, it would be the default document that was displayed: index.cfm. So the first thing we need to do is create a script that will take the requested URL and append "index.cfm" to it, so that CF can process the request.
<cfset requestedURI = listGetAt(cgi.query_string, 2, ";") />
<!--- strip port --->
<cfset requestedURI = Replace(variables.requestedURI, ":80", "", "ALL") />
<!--- if just asking for folder's default doc, append index.cfm --->
<cfif right(variables.requestedURI, 1) is "/">
<cfset requestedURI = variables.requestedURI & "index.cfm" />
<cflocation url="#variables.requestedURI#" addtoken="false" />
<p>This document can not be found.</p>
IIS updates the variable CGI.Query_String to indicate some details about the 404 request, but since we're going to redirect the request, we need to strip out this meta-information. The first thing I do is strip everything left of the first semicolon by treating it as a semicolon delimited list and asking for the second item. (Note that this only works if your URL will never contain a semicolon. You'll have to use other methods if you need semicolons in your URL.) I also strip out the port, which IIS adds, by replacing ":80" with nothing. Once we have a cleaned up URL, we just cflocate to it, and let our onMissingTemplate function take care of the rest. This is all the functionality that we need in our custom 404 handler, so all thats left to do is configure IIS to use it.
To tell IIS to use this template, you need to put it somewhere web-accessible on the site that you want to use it for. I just made mine /404.cfm. Then, in IIS, go to the website properties, custom errors tab, and find the 404 error. Edit it, change the Message Type drop-down from FILE to URL (FILE will only process static files, not dynamic scripts), and specify the absolute path to your new script (/404.cfm).
Now, when a request comes in for myclient.com/foo/, IIS recognizes that the folder doesn't exist and throws a 404 error. The error is handled by our new script, 404.cfm. As we saw above, this script will append index.cfm to the request and clean the URL up a bit, then do a 301 redirect (via cflocation) to what is essentially the same URL, just with the default document added on. This request is passed off to the ColdFusion application server, where it determines that the template doesn't exist, and invokes my onMissingMethod() function. This function recognizes the shop request, determines that it does exist, and passes any business rules, and passes it off to the shop-site request handler, which in turn displays the page.
It's an interesting workflow, and I haven't seen yet how efficient it is under load, but I love the fact that I was able to pull it off.