Pretty URL's SES Support
- Pretty URL's SES Support
- Benefits
- Requirements
- Configuring Your Application For SES Support
- Loose Matching Property
- The routes configuration file
- Adding variables per route
- Numeric Routes
- Optional Variables
- Convention name-value pairs
- Route Examples
- The Default Routes
- Package Resolver
- How do I relocate?
- How about links on my pages?
- How about Form Submissions (Posts)?
- Automating the links
- How about the BASE tag
- Conclusion
ColdBox now supports SES/Pretty URL's support right out of the box thanks to Adam Fortuna's Coldcourse project. ColdBox SES support is based on coldcourse and its rails style routing but taken to an interceptor form and with further enhancements. You will now see an ses interceptor in the core ColdBox interceptors. So now instead of going to urls like:
http://localhost/index.cfm?event=home.about
You can have the same URL available at (Which is our preferred way)
http://localhost/home/about
Benefits
There are several benefits that you will get by using our routing system:
- Complete control of how URL's are built and maintained
- Ability to create or build url's dynamically
- Internal implementation encapsulations and technology hiding
- Greater application portability
- URL's are more descriptive and easier to remember
Requirements
There are no requirements for using the ses interceptor if you will be using the index.cfm file as a routing mechanism:
http://localhost/index.cfm/home/about
However, if you would like to see minimal url's (those without index.cfm in the URL) like:
http://localhost/home/about
Then you must be able to run either apache mod_rewrite .htaccess files, which are enabled with apache, or be running IIS and be able to install an ISAPI filter for URL rewriting. If you have any of these rewrite engines installed then you can use the following rules. Please note that these rules are very basic and might not work on ALL rewrite engines. So as Always, test your rules and expand them.
.htaccess
RewriteEngine on
#RepeatLimit 0
#SQL Injection Protection --Read More www.cybercrime.gov
#Please uncomment to use these rules if below words does not conflict with your friendly-urls. You may modify accordingly.
#RewriteRule ^.*EXEC\(@.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*CAST\(.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*DECLARE.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*DECLARE%20.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*NVARCHAR.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*sp_password.*$ /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*%20xp_.*$ /includes/templates/404.html [L,F,NC]
#if this call related to adminstrators or non rewrite folders, you can add more here.
RewriteCond %{REQUEST_URI} ^/(.*(CFIDE|cfide|CFFormGateway|jrunscripts|railo-context|fckeditor)).*$
RewriteRule ^(.*)$ - [NC,L]
#dealing with flash / flex communication
RewriteCond %{REQUEST_URI} ^/(.*(flashservices|flex2gateway|flex-remoting)).*$
RewriteRule ^(.*)$ - [NC,L]
#Images, css, javascript and docs, add your own extensions if needed.
RewriteCond %{REQUEST_URI} \.(bmp|gif|jpe?g|png|css|js|txt|pdf|doc|xls|xml)$
RewriteRule ^(.*)$ - [NC,L]
#The ColdBox index.cfm/{path_info} rules.
RewriteRule ^$ index.cfm [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.cfm/%{REQUEST_URI} [QSA,L]
IsapiRewrite.ini
IterationLimit 0
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(/.+/.+/.*\?.+\..*)$ /index.cfm/$1
RewriteRule ^(/[^.]*)$ /index.cfm/$1
Once either of those rules are applied, the rewrite engine will correctly route to the ses interceptor.
Some Resources
- http://httpd.apache.org/docs/2.0/misc/rewriteguide.html
- http://cheeso.members.winisp.net/IIRF.aspx
- http://www.isapirewrite.com/
- Micrsoft IIS Rewrite for IIS 7.0
Configuring Your Application For SES Support
Let's start off by setting up SES support in our application. So just open your coldbox configuration file and look for the interceptors elements. If not found, create it like so:
<Interceptors> <Interceptor class="coldbox.system.interceptors.ses"> <Property name="configFile">config/routes.cfm</Property> </Interceptor> </Interceptors>
That interceptor declaration tells coldbox to use the ses interceptor and look for a configuration file in the config folder called routes.cfm. You can name your routes configuration file anything you like.
Loose Matching Property
You can configure the interceptor to do loose matching from the incoming url's by using the LooseMatching property in your configuration file, like we just declared above.
<Interceptors> <Interceptor class="coldbox.system.interceptors.ses"> <Property name="configFile">config/routes.cfm</Property> <Property name="LooseMatching">true</Property> </Interceptor> </Interceptors>
The default value is false. What this directive does, is that it will try to find the routes ANYWHERE in the incoming url instead of starting from the beginning of the url. This can be useful, when you can have loose matching and your URL ROUTE might be in the middle or the end of an incoming url.
For example, I have a route defined as /test/:id-numeric. If I have loose matching turned OFF and I receive the following url:
/index.cfm/test/1233
The route would match to it, but if the incoming url would be
/index.cfm/blog/test/12333
The route would NOT match because it starts with blog and not the route we defined. However, if I turn on Loose Matching, then the last url would match, because the route /test/:id-numeric can be found in the URL anywhere in no specific order.
The routes configuration file
Now that you have defined where the interceptor will find its routes, all you need to do is grab the sample routes.cfm from the Application Template and start modifying it.
Below are the basic methods you will use for SES Routing:
| setEnabled | Enable/Disable routing, enabled by default |
| setUniqueURLS | Enables SES only URL's with permanent redirects for non-ses urls. Default is true |
| setBaseURL | The base URL to use. If using the index.cfm approach, please write it. |
| addCourse | Method to add routes. |
setEnabled
public void setEnabled(boolean enabled)
Parameters:
- enabled
Description: Set whether the interceptor is enabled or not.
setUniqueURLS
public void setUniqueURLs(boolean uniqueURLs)
Parameters:
- uniqueURLs
Description: This determines if non-ses urls should be routed back to the interceptor with permanent redirects. If someone goes to
http://localhost/index.cfm?event=home.main
should we redirect (301, permanantly moved) to the url:
http://localhost/home/main
This will also make sure that any trailing index pages get redirected as well, so if you go to
http://localhost/home/index
it would redirect to
http://localhost/home
In conclusion, if you would like ONLY ses enabled URL's, then set this flag to true.
setBaseURL
public void setBaseURL(string baseURL)
Parameters:
- baseURL
Description:
Used to set the Base URL for your site and URL's when they are built. This is the main location of how ALL your URL's will be prefixed with. If you want your URLs to look like
http://localhost/handler/action
then you should set this variable to the following:
<cfset setBaseURL('http://localhost')>
For this option to work you'll need .htaccess or isapi rewrite support as described above. If you want your URLs to look more like
http://localhost/index.cfm/handler/action
then do the following:
<cfset setBaseURL('http://localhost/index.cfm')>
If you want only SSL enabled URL's, then do this:
<cfset setBaseURL('https://#cgi.http_host#')>
addCourse
public any addCourse(string pattern, [string handler], [string action], [boolean packageResolverExempt='false'], [string matchVariables])
Parameters:
- pattern - The pattern to match against the incoming URL
- handler - The handler to execute if passed.
- action - The action to assign if passed.
- packageResolverExempt - If this is set to true, then the interceptor will not try to do handler package resolving. Else a package will always be resolved.
- matchVariables - A string of name-value pair variables to add to the request collection when this pattern matches. This is a comma delimmitted list. Ex: spaceFound=true,missingAction=onTest
Description:
This is the meat and potatoes for enabling ses routing. You use this method to declare routes that will be dispatched by the interceptor. The syntax of these are similar to Ruby on Rails. The idea is that the number of variables in a URL will be the first indicator of which course to use. Basically, the interceptor goes through each rule in a top-down format and tries to match the incoming URL to the route. If a route matches it will try to create the appropriate event and extra variables in order to respond to a request.
Here's the general setup:
<cfset addCourse(pattern="handler/action/:id", handler="handler_name", action="action_name", packageResolverExempt="false", matchVariables="name-value-pair-list")>
Notice how the pattern argument has three parts. In this case if a request comes in that starts with /handler/action, such as
http://localhost/handler/action/oneOtherItem
then this course will match. The oneOtherItem variable will be set to the request collection as a variable with the name ID
<cfset rc["ID"] = "oneOtherItem">
This can be compared to normal event syntax:
http://localhost/index.cfm?event=handler_name.action_name&id=oneOtherItem
It's important to remember that courses are evaluated in order and go with the first one that matches. For instance, if you have a course for /:handler/:action/:username, and another course later for /:handler/:action/:id, then the second course will NEVER be run. If instead you change your first course to something more generic such as /user/:action/:username, and specify (handler="user") as a second parameter, then only URLs that start with /user will qualify for having the third argument of username, while everywhere else will use a third argument of ID. Best Practice: always try to create meaningful courses that can be distinct in order to increase the number of course permutations.
By the way, what's with the :' syntax? In the Ruby programming language, any variable starting with a : is a symbol. Symbols are just like strings but they always point to the same place in memory and are therefore more efficient. They don't work that way here in ColdFusion, but they make a good variable marker without worrying about where to put quotes and stuff. ColdBox takes this to a new level by adding some extra features.
- :varname This is how we do URL placeholders in CodlBox SES.
- In order to have a route with a :handler placeholder to NOT resolve directories or packages, use the packageResolverExempt argument (Set it to TRUE).
Adding variables per route
You can add variables to the request collection by using the matchVariables argument or by adding your extra name-value pairs to the method as arguments. The matchVariables argument is a simple string of name-value pairs that you can pass to the route definition. This basically tells the ses processor that if that specific route matches, then add those name-value pairs to the request collection. This is a great way to add YOUR OWN variables hidden from the URL/FORM.
Example:
<cfset addRoute(pattern="space/:space",handler="page",action="show",matchVariables="spaceUsed=true,foundAt=#now()#")>
As mentioned above, you can also add variables by just adding them as arguments to the addCourse() method. This is more by convention and it has the limitations that you cannot use the following as named arguments as they are already arguments: pattern,handler,action,packageResolverExempt and matchVariables.
Example: Let's say I want to add the following variables to my route if it is found:
- foundAt = #now()#
- internalNamespace = "_internal"
- hashMap = structnew()
Then I would setup a route like so:
<cfset addRoute(pattern="page/:page",handler="page",action="show",foundAt=now(),internalNamespace="_internal",hashMap=structnew())>
Numeric Routes
ColdBox gives you also the ability to declare numeric only routes alongside the normal alphanumeric routes by appending -numeric to the variable placeholder.
Numeric Route:
/blog/:year-numeric
Alphanumeric Route :
/blog/:year
That's it. Everytime you append the -numeric tag, the placeholder will only work if its numeric.
Optional Variables
ColdBox 2.6 also introduces optional variables for your placeholders. Most of the time we need to create several routes in order to determine possible routings. A great example is the declaration of the following:
/:handler/:action/:id /:handler/:action /:handler
We just wrote 3 routes for this when we can just use optional variables by using the ? symbol at the end of the placeholder.
/:handler/:action?/:id?
This syntax will create the three routes for you, but you only need to declare 1. Just remember that an optional placeholder cannot be followed by a non-optional one. It doesn't make sense. So remember, just append a ? to make a placeholder optional.
/blog/:year-numeric/:month-numeric?/:day-numeric?
Convention name-value pairs
ColdBox introduces name-value pairs by convention by inspecting the incoming URL. This means that after a route has been matched and there are still values in the request string, the interceptor will try to create name-value pairs out of them. Example, if we have the following route:
addRoute(':handler/:action')
Then if we have the following url: index.cfm/users/list/page/2/issues/5 Then the interceptor would route it to the event = users.list and nothing more. With convention name-value pairs, the interceptor will try to create name-value pairs from the remaining string, in our case: page/2/issues/5. So the interceptor will create the following variables in the request collection for you, without YOU doing anything:
- page = 2
- issues = 5
Is this cool or what, you can easily create more custom routes and even have convention routing, all provided for free. Of course, if there are no name-value pairs, they won't be created, or if the name is missing its value, it won't be created.
Route Examples
<cfset addCourse(pattern="blog/entry/:year/:month-numeric?/:day-numeric?", handler="blog", action="entry" )> <cfset addCourse(pattern="profile/view/:username", handler="profile", action="view" )> <cfset addCourse(":handler/:action?/:id?")>
The Default Routes
As you can see from the sample above, the last route is actually the default route that your application should have
<cfset addCourse(":handler/:action?/:id?")>
So by having these default routes, you can easily route to any handler-action combination, or just handler combo. Very easy and snappy to use.
Package Resolver
From the route above we know that a url can look like this: index.cfm/handler/action, but what if I want to execute a handler within a package? Well, there are mostly two approaches to this:
- Use the built-in package resolver (Automatically done for you)
- Create specific routes to your handler
The first approach is done automatically for you by just using the default route of :handler/:action?/:id?. When ColdBox detects the incoming URL and this route is matched, it will try to expand the placeholder for the handler and see if it is a directory or not. If it is a directory, it will save it and continue parsing the URL until a handler is matched. Mumbo Jumbo, give me samples. Ok Ok, here you go.
Handler Structure
+handlers
+ admin
+ user.cfc
+ general.cfc
In previous 2.6.2 versions, a URL to a list action for the admin user controller would look like this:
index.cfm/admin.user/list
This works great, but it is not that user friendly and I think we could do better. Therefore, since version 2.6.2, ColdBox can resolve packages. So the URL now looks like this:
index.cfm/admin/user/list
WOW!! That does look better and achieves better SEO than the other one. So there you go, you can add as many packages until you reach your handler. So what about the second way you mentioned about creating specific routes. Well, for this to work we must add the route ourselves:
<cfset addRoute(pattern="/admin/user/:action?",handler="admin.user")>
As you can see from the pattern above, I created a specific route of admin/user/ and this becomes the alias for the admin.user handler. I could have easily renamed the alias but my handler is hidden from the URL. So the question is, which one should I use? Well, the best practice is to always hide implementation, so I would go with the latter solution as it provides a better way to refactor the application without changing url's.
How do I relocate?
The next important part of your paths into SES is that you need to relocate to other events and also create links. So how do I do this? By using the following two methods:
- setNextEvent([string event], [string queryString], [boolean addToken], [string persist], [struct varStruct], [boolean ssl],[string baseURL])
- setNextRoute(string route, [string persist], [struct varStruct],[boolean addToken],[boolean ssl])
setNextEvent
The setNextEvent method can be used for both normal and ses urls, here are its parameters:
| Argument | Required | Type | Description |
| event | false | string | The event to relocate to, if empty it defaults to the default event. ex: main.home |
| queryString | false | string | The query string to append to the relocation |
| addToken | false | boolean | Whether to add the cf tokens or not. Default is false |
| persist | false | string(list) | A comma-delimited list of request collection key names that will be flash persisted in the framework's flash RAM and re-inflated in the next request. |
| varStruct | false | struct | A structure of key-value pairs that will be flash persisted in the framework's flash RAM and re-inflated in the next request. |
| ssl | false | boolean(false) | Flag indicating if redirect should be done in ssl mode or not |
| baseURL | false | string | If used, then it is the base url for normal syntax redirection instead of just redirecting to the index.cfm |
All the arguments above are pretty straightforward except: persist & varStruct. Persist can be a comma-delimited list of request collection' key names that you want to persist in the internal flash memory of the framework for the relocation.varStruct is a structure of key-value pairs that will be persisted across a request. So when the user get's relocated, those variables (can be simple, complex, or even objects) will be flash stored and re-inflated back to the request collection on the relocation. Very useful to silently keep state on temporary objects.
setNextRoute
The setNextRoute method can be used for ses urls ONLY and it can use the following arguments:
| Argument | Required | Type | Description |
| route | true | string | The route to relocate to. ex: main/home |
| persist | false | string(list) | A comma-delimited list of request collection key names to persist in the relocation |
| varStruct | false | struct | A structure of key-value pairs that will be flash persisted in the framework's flash RAM and re-inflated in the next request. |
| addToken | false | boolean(false) | Flag to add the CF tokens to the redirection |
| ssl | false | boolean(false) | Flag indicating if redirect should be done in ssl mode or not |
Now, please note that this method is only for routing. If you want to use a method that can encompass both scenarios then use setNextEvent(). Since version 2.6.2, setNextEvent() supports both normal event syntax and route redirection. The persist and varStruct arguments apply the same as setNextEvent().
Below are some samples:
<cfset setNextRoute("handler/action")> <cfset setNextEvent('handler.action')> <cfset setNextRoute("home/about")> <cfset setNextEvent('home.about')> <cfset setNextRoute("blog/entry/223443")> <cfset setNextEvent(event='blog.entry.223443')>
How about links on my pages?
Well, you would use the settings that the interceptor places for you in your configuration structure and write the absolute URL by hand or just use the best method in the world: event.buildLink()
<a href="#getSetting('sesBaseURL')#/home/about">About</a> <a href="/home/about">About</a> <!--- The best one is ---> <a href="#event.buildLink('home.about')#">About</a>
How about Form Submissions (Posts)?
As you are now using SES, the ses interceptor will parse all requests, so you have to accurately change your posting actions to the correct processor. You cannot just post it to the index with an event variable. You need to post to the correct ses action.
<form action="index.cfm/handler/action" method="post" name="testform"> All your form elements here </form> <!--- OR the following ---> <form action="#event.buildLink('handler.action')#" method="post" name="testform"> </form>
So as you can see from the sample, the action points to: index.cfm/handler/action if you are not using the index.cfm processor then it would be: /handler/action.
Automating the links
The best way to automating the writing of the links in your application is to use the event.buildLink() method that is available in the event object. This method is used to create links for you whether you are in SES mode or not. The other cool thing, is that you can keep your event notation and the method will translate it to routes for you.
- buildLink(string linkto, [boolean translate], [boolean ssl], [string baseURL])
Arguments
| argument | type | required | default | description |
| linkTo | string | true | --- | the event + query string combination |
| translate | boolean | false | true | If set to true it will translate any . to / if in SES mode. If false, it will not translate |
| ssl | boolean | false | false | creates the url link in ssl mode |
| baseURL | string | false | --- | Only applicable in non-ses mode. The base full URL path to append to index.cfm |
How about the BASE tag
Well, for the base tag you can use either the sesBaseURL setting, if using minimal URL support, or the htmlBaseURL setting if using the index.cfm in the URL. Both of these settings are set by the interceptor at configuration time and available for your usage via the getSetting() method.
<base href="#getSetting('htmlBaseURL')#"> <base href="#getSetting('sesBaseURL')#">
Conclusion
As you can see, SES support in ColdBox is a breeze. Very easy to configure and use:
- Declare your interceptor
- Create your routes configuration file and create your routes
- Code away with the new setNextRoute() method and your two new settings: sesBaseURL & htmlBaseURL
