Ask Ben: Dynamic Web Root And Site URL Calculations In Application.cfc
I'm new to using Application.cfc. Following Ben Forta's tutorial, I've created one that calls a SiteHeader and a SiteFooter. The header has an image file which displays at the root level of the website, but breaks when you go to folders underneath that. I can make it work by setting a Request.Image_Path in Application.cfc, but only by hardcoding the URL. Works okay in localhost mode, but if I move the files to a production webserver, I have to remember to change the path to the images directory. I'm sure there's a smarter way to do this, but I haven't been able to figure it out.
Sometimes, nothing makes me sadder than seeing URLs hard coded in ColdFusion code. Sure, if we all ran on our web servers on Apache and were server administrators, then maybe hardcoding our web roots might make sense. But, for those of us that don't run on Apache and are not web server administrators, hardcoding URLs, as you are seeing, can only lead to problems. The good news is, in ColdFusion, it's actually super easy to dynamically calculate the web root and the site URLs so long as you understand the relationships between the various paths available to you.
Before we get into how these calculations are made, let's take a look at the web page that we want to render. The following is my index.cfm which will be included by my OnRequest() event handler (in the Application.cfc) no matter what page was requested by the user. This page is, essentially, our Front Controller:
Index.cfm (Front Controller)
<cfoutput>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Dynamic Web Root ColdFusion Example</title>
</head>
<body>
<h1>
Dynamic Web Root ColdFusion Example
</h1>
<h2>
CGI.script_name
</h2>
<p>
#cgi.script_name#
</p>
<h2>
Site URL
</h2>
<p>
#request.siteUrl#
</p>
<h2>
Web Root
</h2>
<p>
#request.webRoot#
</p>
<h2>
Sample Image
</h2>
<p>
<!---
Notice that we start the image SRC attribute with
our web root. That will start the SRC path with
whatever "../" path traversals as is necessary to
get to the web root.
--->
<img
src="#request.webRoot#jen_rish.jpg"
width="400"
height="267"
alt="Jen Rish - Holy Legs Batman!"
/>
</p>
</body>
</html>
</cfoutput>
On this test page, I am outputting several values:
CGI.script_name: This is the script / template that was requested by the user (remember, we are executing index.cfm no matter what template they requested).
Site URL: This is the URL of our site which will be the root level directory of our web application. Keep in mind that on a development machine that is host to many local web sites, this might be a directory nested deeply within the actual web root of the web server. Therefore, it is important that our site URL is not the same we are server's web root!
Web Root: This is the path traversal string in the form "../../" that will be required to get to the web root of our application relative to the template requested by the user. Each directory down from the application web root will require an additional "../" traversal.
Sample Image: This is an image that lives in the web root of the site. I have put this here to demonstrate how the web root string determined above must be put in front of each relative path to make the URLs work.
While you haven't seen the Application.cfc file just yet (to come later), I have implemented the OnMissingTemplate() event handler such that I can simulate nested directories simply by changing the URL of the requested page. Assuming that my web application is located in the following directory:
/personal/ben/kinky_solutions/testing/webroot/
... I'm going to navigate to the given URL:
/personal/ben/kinky_solutions/testing/webroot/sub1/sub2/index.cfm
Notice that this URL is two directories down from the web root (/sub1/sub2/). In order for the image in our front controller (index.cfm) to render properly, the web root that we calculate must be "../../". And, in fact, when we navigate to the URL above, you can see that it does:
As you can see in the screen shot, even from within the "sub1/sub2/" directory, the web root was calculated properly to be "../../" and our site url was calculated properly to point to the "/webroot/" directory. This will work no matter what URL I use.
Now that we've seen this in action, let's take a look at the Application.cfc file to see how these calculation are being made:
Application.cfc
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 1, 0 ) />
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!--- Define arguments. --->
<cfargument
name="template"
type="string"
required="true"
hint="I am the template requested by the user."
/>
<!--- Define the local scope. --->
<cfset local = {} />
<!--- Define request settings. --->
<cfsetting showdebugoutput="false" />
<!---
Set the value of the web root. Since we know that this
template (Application.cfc) is in the web root for this
application, all we have to do is figure out the
difference between this template and the requested
template. Every directory difference will require our
webroot to have a "../" in it.
--->
<!---
Get the current (Application.cfc) directory path based
on the current template path.
--->
<cfset local.basePath = getDirectoryFromPath(
getCurrentTemplatePath()
) />
<!---
Get the target (script_name) directory path based on
expanded script name.
--->
<cfset local.targetPath = getDirectoryFromPath(
expandPath( arguments.template )
) />
<!---
Now that we have both paths, all we have to do is
find the difference in path. We can treat the paths
as slash-delimmited lists. To do this, let's calculate
the depth of sub directories.
--->
<cfset local.requestDepth = (
listLen( local.targetPath, "\/" ) -
listLen( local.basePath, "\/" )
) />
<!---
With the request depth, we can easily create our
web root by repeating "../" the appropriate number
of times.
--->
<cfset request.webRoot = repeatString(
"../",
local.requestDepth
) />
<!---
While we wouldn't normally do this for every page
request (it would normally be cached in the
application initialization), I'm going to calculate
the site URL based on the web root.
--->
<cfset request.siteUrl = (
"http://" &
cgi.server_name &
reReplace(
getDirectoryFromPath( arguments.template ),
"([^\\/]+[\\/]){#local.requestDepth#}$",
"",
"one"
)
) />
<!--- Return true so the page can execute. --->
<cfreturn true />
</cffunction>
<cffunction
name="onRequest"
access="public"
returntype="void"
output="true"
hint="I execute the final template.">
<!--- Define arguments. --->
<cfargument
name="template"
type="string"
required="true"
hint="I am the template requested by the user."
/>
<!---
Include the our front-controller no matter which
template was requested by the user.
--->
<cfinclude template="./index.cfm" />
<!--- Return out. --->
<cfreturn />
</cffunction>
<cffunction
name="onMissingTemplate"
access="public"
returntype="boolean"
output="true"
hint="I handle any CFM/CFC 404 error requests.">
<!--- Define arguments. --->
<cfargument
name="template"
type="string"
required="true"
hint="I am the template requested by the user."
/>
<!---
Call the request event methods to process the page as
if it was called normally.
--->
<cfset this.onRequestStart( arguments.template ) />
<cfset this.onRequest( arguments.template ) />
<!---
Return true to make sure that the 404 page not found
error does not get executed.
--->
<cfreturn true />
</cffunction>
</cfcomponent>
The real magic in this Application.cfc takes place in the OnRequestStart() event handler. The OnMissingTemplate() event handler is just there to help me simulate nested sub directories and the OnRequest() event handler is there simply to execute our front controller; all of the calculations take place in OnRequestStart(). As you can see in the code, all this really comes down to is a difference in the path depth between our based directory (the one containing the Application.cfc ColdFusion component) and our target directory (the one containing the user-requested script). Once we have this depth difference, all we have to do is build our web root by repeating the sting, "../", one per sub-directory depth.
The only real trick here is understanding that we have to expand the path of the target script name in order for it to be comparable to the path gotten via GetCurrentTemplatePath(). Once we've expanded it, however, we can simply treat both paths as slash-delimited lists and compare their relative lengths to get depth of the given request.
Once we have the path depth, I am also using it to calculate the main URL of the site. While you wouldn't do this for every request - it's a value that could and should be cached during Application initialization - I wanted to demonstrate how you could take the script name and the request depth to easily walk back up to the main directory of the site to find the URL of the root application.
With just a few lines of code, we can easily calculate our web root and our site URL for every page request. By keeping this value dynamic and not hard-coding our URLs, it keeps our applications very portable. This allows other developers, especially within a team environment, to easily check out code bases and get things up and running in no time without being web server gurus. I hope this helps!
Want to use code from this post? Check out the license.
Reader Comments
Cool stuff Ben! I just have a couple words of caution for the sake of SEO (search engine optimization) concerns. When you use URL rewriting or even just the common CFML friendly URL formats (like /blog/index.cfm/category/page-title-alias/), you can get into trouble with relative paths to static assets (e.g., img src="../images/logo.gif").
For example, if someone hits the URL path /blog/index.cfm/category/page-title-alias/ and you want your (X)HTML to display an image located at /images/logo.gif, you can't use src="../images/logo.gif" -- Apache (or IIS or whatever) will be looking for /blog/index.cfm/category/images/logo.gif, which does not exist.
Instead, I've found it helpful and more flexible to always use an absolute path from the Web root for all static assets (images, css, js, etc.), which would be src="/images/logo.gif" for the preceding example.
So, it might be worth calculating something like this, or just hard-code a property or two for relevant static assets. Then, for example, you might set imagePath="/images/" somewhere and have src="#imagePath#logo.gif"
You could take this a step further and be completely explicit by including a variable to hold the base URL (http://host/) as well, and you'd then have src="#baseURL##imagePath#logo.gif" -- This may seem like overkill at first, bit if you might want to scale to have static assets served up by a different Web server or something like Amazon S3 in the future, then you need only change one property to update your entire app :)
All that said, it can often still make sense to do some URL and path calculations at application startup and store these properties. It's also great to introduce new CFers to functions like expandPath(), getDirectoryFromPath(), getCurrentTemplate(), etc. -- just to have them at the ready. I know I spent my first couple years in CF with hard-coded paths in my Application.cfm before I discovered these gems!
Ooh! One more... Keep in mind that CF is running on Java. When working with any file system paths you should *always* use forward slashes -- *never* backslashes -- backslashes are for Windows only, forward slashes work with any OS, including Windows!
Okay, enough rambling comments ;-)
I always use absolute path on my assets.
@Jamie,
I am all for using stored paths, as I have done in this demo. I used "webRoot", but I think you raise a good point that if you make it more specific to the intent, such as "imageRoot", then you could easily switch to things like AS3 - great point!
However, I just want to caution that using absolute paths such as "/images" should also be calculated, otherwise you will run into the same problem that the reader described - that it might be "/images" on production and "/sites/mysite/www/images" on development. Either way, you do *not* want to hard code this value.
@Derek,
How do you deal with this across servers? Or are you running Apache with individual sites?
Using IIS for the most part.
My dev and live environments will usually mimic each other as much as possible.
I always use a host name on dev like http://site1 http://site2 and use the hosts file to point them all to 127.0.0.1 and put a host header in IIS for each site.
So /images is always /images for me.
at the same time, I don't really want to say this, I do hard code the paths at this time. [embarrassed]
If you do an example using an absolute path, maybe I can change that. ;)
@Derek,
I've looked into host files, but from what I read, you could only use one site at a time? I would love to see an example of what you are doing.
oh, i should mention, I am using Vista. No IIS site limitations. If you are on XP, move along, nothing to see here. Are you using XP?
Typo line 3 of your Application.cfc
hitn = hint
(Thanks for this solution Ben, it's simple and elegant! Sadly, I'm like Derek and avoided the problem my having my production and development mirror each other, but it's definitely not the best practice.)
@Derek,
I started on Vista... then switched back to XP because I didn't no how much longer I could resist throwing my computer out of the window :P
I wonder why it would have no IIS limitations. That would be an awesome feature across the board.
@Doug,
Thanks, typo fixed. The problem with any kind of server-dependent configuration is that, me being on XP, I could never work on the same team with Derek as I would not be able to get the same code base to run on my machine.
Of course, this stuff is only a problem *when* it's a problem.
I guessed too many ppl were pissed that they did that in XP. So they removed the limitation. Also, I believe they didn't want ppl buying XP to use as server OS. ppl won't do that with Vista. Too much overhead.
@Derek,
Ahh, gotcha. It never occurred to me to try an use XP to run a server. I guess that would make sense. I've never worked on anything but Windows Server (live).
@Doug, are you saying it's not best practice to mimic your Live environment to DEV and QA and/or staging? If so, I completely disagree. I think they need to mimic each other as best as possible. I know in a personal dev enviroment ur not going to have QA and staging most likely, but in a Enterprise environment, it should be the norm. Staging should be an exact replica of Live right down to the hardware. QA and DEV you can get away with not having the same hardware, but should be exact same site structure. [saying this as i sit here @work where nothing is done right at all]
@Derek,
That said, it's easier to have sites mimic each other if paths are calculated :)
Usually, I have my staging site on the same machine as the live site. Not sure if that is a best practice, but it seems to be the easiest and most useful thing to do.
I am still waiting for the example with absolute path calculated. ;)
@Derek,
Let me see what I can do at lunch :)
@Derek
I have a problem with making sweeping statements, so you're right in a sense. What I meant to say was more about the relationship between Development--and I mean the earliest levels of development--and live.
If I am a freelancer or even if I am dabbling in design on my home computer, I want to have a testing environment for my projects that can at least help me test if my CF and HTML are working. This way once I bring in my product I know it will work in any environment and I won't be forced to make massive find/replace searches which inevitably miss something.
So I think it is best practice to make sure that my code is capable of saying "I am here" instead of me having to tell it, "You are here." It makes the code more transferable and scalable thus people can then trade it around regardless of what structure of the live, QA, or staging servers have.
(Thanks to virtualization, all the servers I currently work with are identical, so this is more an exercise of "what if"; making sure that if I ever do have to freelance, I'll have the skills necessary to make all my code scalable/transferable/awesome.)
This works on CF7 if you change "<cfset local = {} />" to "<cfset local = structNew() />"
@Ben
I followed your code examples above and was able to get the #request.webroot# to work. This is the code at 5 levels deep:
<body>
<img src="../../../../images/logo_c.gif" width="101" height="101" align="absmiddle" border="0" />
<span class="header">Orange Whip Studios</span>
<ol>
<li>CGI.SCRIPT_NAME -- /ows/test/level2/level3/level4/level5/index.cfm</li>
<li>request.siteURL -- http://localhost/ows/test/</li>
<li>request.webRoot -- ../../../../</li>
<li>request.datasource -- ows</li>
</ol>
<img src="../../../../3649063362_9c689377ca.jpg" />
<hr />
<hr />
<p class="footer">
(c) 2009 Orange Whip Studios. All rights reserved>
</p>
<img src="../../../../images/creek.jpg" />
</body>
</html>
I'm new to using CFCs, so I've been working with Ben Forta's CF8 book. My goal is to include a header and footer in the Application.cfc using the technique you showed me. However, I'm getting pages with no content except for the header and footer. If you look at the page example above, there's supposed to be content between the two <hr /> markers. So, I've obviously missed something in the coding between your example and the one Ben uses in his book.
@Alan,
Thanks, I was having problems with that on CF7.
@Paul,
I'd have to look at your Application.cfc file. Maybe ZIP it up and submit via my Ask Ben form.
I hate this path stuff so much in CF that I am moving to .Net
good post. nice article