<< Back to Dashboard

Contents

URL Mappings (SES Support)

Covers up to version 3.5.0

Introduction

ColdBox URL Mappings will give you support for creating Search Engine Safe (SES) URLs, RESTful services, and overall URL routing your way. We would love to take credit for this feature, but it was inspired by Rails and our good friend Adam Fortuna's ColdCourse project. By convention URL mapping support will allow you to create URL's without using the ?event=this.that&param1=val formats by more like the following example:

// Old Style
http://localhost/index.cfm?event=home.about&page=2
http://localhost/index.cfm?city=24&page=3&county=234324324

You can have the same event but with a URL like this:

// Routing Style
http://localhost/home/about/page/2
http://localhost/dade/miami/page/3

What is a route?

A route is a declared URL pattern that if matched it will translate such URL into either an event or a view to be dispatched.

Benefits

There are several benefits that you will get by using our routing system:

Requirements

By default all ColdBox application templates and generated applications have SES support built in via our SES interceptor. This is declared in your configuration file ColdBox.cfc. This will allow you to build URI's by fowarding them through the index.cfm.

Important : Some J2EE servlet containers do not support the forwarding of SES parameters via the routing template out of the box. You might need to enable full URL rewriting either through a web server or a J2EE filter.

http://localhost/index.cfm/home/about

However, if you would like to see minimal URI's (those without index.cfm in the URL) like:

http://localhost/home/about

Then you will need to enable URL rewriting at the web server level or use a J2EE rewrite filter. The most common are listed below:

Some Resources

Our ColdBox bundle download includes an SES folder that provides you with all the necessary rewrite rules for all the resources mentioned.

Rewrite Rules

Here are just a few of those rewrite rules for you:

.htaccess

RewriteEngine on

# Bypass call related to adminstrators or non rewrite folders, you can add more here.
RewriteCond %{REQUEST_URI} ^/(.*(CFIDE|cfide|CFFormGateway|jrunscripts|railo-context|mapping-tag|fckeditor)).*$
RewriteRule ^(.*)$ - [NC,L]

# Bypass flash / flex communication
RewriteCond %{REQUEST_URI} ^/(.*(flashservices|flex2gateway|flex-remoting)).*$
RewriteRule ^(.*)$ - [NC,L]

# Bypass 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|ico)$
RewriteRule ^(.*)$ - [NC,L]

# The ColdBox index.cfm/{path_info} rules.
RewriteRule ^$ index.cfm [QSA,NS]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.cfm/%{REQUEST_URI} [QSA,L,NS]

IsapiRewrite.ini

# Helicon ISAPI_Rewrite configuration file
# Version 3.1.0.48
RewriteEngine On
RepeatLimit 0

#dealing with cf-administrator,railo-administrator, etc
RewriteRule ^/(CFIDE|cfide|CFFormGateway|jrunscripts|railo-context|fckeditor) - [L,I]

#dealing with flash / flex communication, cf-administrator,railo-administrator, etc
RewriteRule ^/(flashservices|flex2gateway|flex-remoting) - [L,I]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.cfm/%{REQUEST_URI} [QSA,L]

IIS7 web.config

<configuration>
    <system.webServer>
        <rewrite>
            <rules>
               <rule name="Application Adminsitration" stopProcessing="true">
                    <match url="^(.*)$" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{SCRIPT_NAME}" pattern="^/(.*(CFIDE|cfide|CFFormGateway|jrunscripts|railo-context|fckeditor)).*$" ignoreCase="false" />
                    </conditions>
                    <action type="None" />
                </rule>
                <rule name="Flash and Flex Communication" stopProcessing="true">
                    <match url="^(.*)$" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{SCRIPT_NAME}" pattern="^/(.*(flashservices|flex2gateway|flex-remoting)).*$" ignoreCase="false" />
                    </conditions>
                    <action type="Rewrite" url="index.cfm/{PATH_INFO}" appendQueryString="true" />
                </rule>
                <rule name="Static Files" stopProcessing="true">
                    <match url="^(.*)$" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{SCRIPT_NAME}" pattern="\.(bmp|gif|jpe?g|png|css|js|txt|pdf|doc|xls)$" ignoreCase="false" />
                    </conditions>
                    <action type="None" />
                </rule>
                <rule name="Insert index.cfm" stopProcessing="true">
                    <match url="^(.*)$" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="index.cfm/{PATH_INFO}" appendQueryString="true" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

SES Interceptor

The SES interceptor is the class in ColdBox that provides you with URL Mapping and RESTful support. You will have this declared (or need to declare) in your ConfigurationCFC:

interceptors = {
  {class="coldbox.system.interceptors.SES"}
};

By convention the interceptor will look for config/Routes.cfm as your configuration file. If you want to change this, then declare a property of the interceptor with a path to your configuration file:

interceptors = {
  {class="coldbox.system.interceptors.SES",
   properties = {configFile = "myconfig/path/Routes.cfm"} }
};

Once the SES interceptor loads in your application it will create two settings for you:

Routes Configuration

Inside of your Routes.cfm template is where you will use our routing DSL (Domain Specific Language) to define configuration parameters for your routing, RESTful URIs and to create URL mappings.

Important: The routes configuration file gets executed within the interceptor, so ALL interceptor methods are available for your usage. You can use tier detection, settings, etc.

Configuration Methods

You can use the following methods to fine tune the configuration and operation:

Method Description
setEnabled( boolean ) Enable/Disable routing, enabled by default
setUniqueURLS( boolean ) Enables SES only URL's with permanent redirects for non-ses urls. Default is true, but we highly encourage you to always use false to allow for dual types of URLs. If true and a URL is detected with ? or & then the application will do a 301 Permanent Redirect and try to translate the URL to a valid SES URL.
setBaseURL( string ) The base URL to use for URL writing and relocations. This is usally includes the web address to your application wether it is in the root or embedded in a folder. e.g. http://www.coldbox.org/, http://mysite.com/index.cfm''
setLooseMatching( boolean ) By default URL pattern matching starts at the beginning of the URL, however, you can choose loose matching so it searches anywhere in the URL.
setExtensionDetection( boolean ) By default ColdBox detects URL extensions like json, xml, html, pdf which can allow you to build awesome RESTful web services.
setValidExtensions( list ) Tell the interceptor what valid extensions your application can listen to. By default it listens to: json,jsont,xml,cfm,cfml,html,htm,rss,pdf
setThrowOnInvalidExtensions( boolean ) By default ColdBox does not throw an exception when an invalid extension is detected. If true, then the interceptor will throw a 406 Invalid Requested Format Extension: {extension} exception.
setAutoReload( boolean ) By default all URL mapping routes are processed on application startup and cached. You can enable auto reloading of the routes in each request just in case your are in development and want to see change of URL mappings immediately. TURN TO FALSE IN PRODUCTION
includeRoutes( path ) Gives you the ability to include other routing configuration files. Great for separation and module SES rewriting.
setUniqueURLS(false);
//Auto reload configuration, true in dev makes sense to reload the routes on every request
setAutoReload(false);
// Sets automatic route extension detection and places the extension in the rc.format variable
setExtensionDetection(true);
// The valid extensions this interceptor will detect
setValidExtensions('xml,json,rss,pdf,html,htm');
// If enabled, the interceptor will throw a 406 exception that an invalid format was detected or just ignore it
setThrowOnInvalidExtension(true);

// Base URL
if( len(getSetting('AppMapping') ) lte 1){
  setBaseURL("http://#cgi.HTTP_HOST#/index.cfm");
}
else{
  setBaseURL("http://#cgi.HTTP_HOST#/#getSetting('AppMapping')#/index.cfm");
}

// Include some additional routes
includeRoutes('config/package1')
  .includeRoutes('config/admin')

All SES methods can be concatenated with each other

Base URL

The base URL is a very important configuration as that value is used when relocating and building URLs in the application. So if you are not using a rewrite engine then you will need to add the index.cfm to the setting. If you are using a rewrite engine, then just leave the domain entry point to the application. If you will be using the same application in a multi-tenant application, meaning one application via multiple domains, then one SES Base URL doesn't really make sense as the first domain request takes precedencce. For this, you will need to modify the SES Base URL setting on a request basis. We recommend building an interceptor for this, we have one already for you in your awesome code repository, ForgeBox. The interceptor basically tells the request context what URL to use for that request.

component{
  
  function preProcess(event,interceptData){
    event.setSESBaseURL( "http://" & cgi.http_host );
  }

}

URL Mappings

URL mapping or routing is done via our routing methods:

Adding Routes

public any addRoute(string pattern, [string handler], [any action], [boolean packageResolverExempt='false'], [string matchVariables], [string view], [boolean viewNoLayout='false'], [boolean valuePairTranslation='true'], [any constraints=''], [string module=''], [string moduleRouting=''], [string namespace=''], [string namespaceRouting=''], [boolean ssl='false'], [boolean append='true'])
Argument Type Required Default Description
pattern string true --- The URL pattern to look for in the incoming URL
handler string false --- The handler to translate to, can include module or package path
action string or struct false --- The action to translate to. If a structure, then the keys represent the HTTP verbs of the RESTful action to relocate to.
packageResolverExempt boolean false true Automatically can resolve packages in the URL when using routing by convention
matchVariables string false --- A query string of variables to inject into the request collection if the route matches
view string false --- The name of the view to dispatch the URL to instead of event routing
viewNoLayout boolean false false Use no layout when rendering the view dispatched or not
valuePairTranslation boolean false true By default it converts any name-value pair after the matched URL pattern to the request collection
constraints struct false {} A structure of regex constraints for variable placeholders in the URL patterns
module string false --- Add this route to a named module
moduleRouting boolean false false Called internally by addModuleRoutes to add a module routing route only.
namespace string false --- The namespace to add this route to
ssl boolean false false Makes the route SSL only if true, else for either SSL or non SSL. If SSL, the interceptor will relocate to the same route but in SSL.
append boolean false true By default all routes are stored in first come first served order, but you can pre-pend to the first position if so desired.

This is the meat and potatoes for enabling 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.

Note: You can pass any named argument and value to this method and the interceptor will create a new variable with the name of the argument in the request collection for you. This can be used as an alternative to using the matchVariables argument.

Routing By Convention

The default route is:

addRoute(pattern=":handler/:action?");

The URL pattern in the default route includes two special position holders:

With one route you can potentially write an entire application because once a route is deteceted, any extra name-value pair in the URL will be translated to variables in the request collection for you.

http://localhost/general -> event=general.index
http://localhost/general/index -> event=general.index
http://localhost/general/index/id/2 -> event=general.index&id=2
// If 'admin' is a package in the handlers directory
http://localhost/admin/general/index -> event=admin.general.index 
// If 'admin' is a module
http://localhost/admin/general/index/id/4/page/2 -> event=admin:general.index&id=4&page=2

With one route you write all your URLs. However, the problem is that your routing URLs depend too much in the name of your handlers and actions. Thus, if you refactor, your URLs change and that's not very nice. That's why we encourage you to create more routes so this can be avoided especially on public sites.

Handler Routing

Once you declare a route you will most likely route it to an event by using the handler and/or action arguments:

addRoute(pattern="/blog", handler="blog", action="index");

This create the event=blog.index translation for you.

RESTful Action Routing

You can also use a structure for the action argument so you can split the incoming HTTP method verbs into the appropriate actions:

addRoute(pattern="/user/:username", handler="users", action={
  GET = "list", POST = "create", PUT = "save", DELETE = "remove", HEAD ="info"
});

Isn't that cool! Just like that you can split the incoming URL pattern to appropriate action executions according to the incoming HTTP verb.

View Routing

You can also route a URL pattern to a view with its default or assigned layout or none at all:

addRoute(pattern="/contact-us", view="static/contact");
addRoute(pattern="/newsletter", view="static/newsletter", noLayout=true);

Pattern Placeholders

In your URL pattern you can also use the same : syntax to denote a variable position holder. These position holders are alpha-numeric by default:

addRoute(pattern="blog/:year/:month?/:day?");

Once a URL is matched to the route, those placeholders will become request collection variables:

http://localhost/blog/2012/12/22 -> rc.year=2012, rc.month=12, rc.day=22

Numeric Placeholder

ColdBox gives you also the ability to declare numeric only routes by appending -numeric to the variable placeholder so the route will only match if the placeholder is numeric.

/blog/:year-numeric/:month-numeric?/:day-numeric?

That's it. Everytime you append the -numeric tag, the placeholder will only match if its numeric.

Alpha Placeholder

ColdBox gives you also the ability to declare alpha only routes by appending -alpha to the variable placeholder so the route will only match if the placeholder is alpha only.

/wiki/:page-alpha

Dynamic handler/action Placeholders

You can also use the reserved position placeholders :handler and :action so you can specifically position them in the URL dyamically, meaning their names comes from the URL:

// route with regex constraint
addRoute(pattern="/admin/users/:action", handler="admin.users");
addRoute(pattern="/admin/:handler/:action?");

Regular Expression Placeholder

There are two ways to place a regex constraint on a placeholder, using the regex: placeholder or adding a constraints structure to the route declaration.

regex:()
// route with regex constraint
addRoute(pattern="/api/regex:(^(xml|json))/",
         handler="api",
         action="execute");

Error parsing plugin definition: The MESSAGE parameter to the renderit function is required but was not passed in.

constraints

The key in the structure must match the name of the placeholder and the value is a regex expression that must be enclosed by parenthesis ()'.

// route with custom constraints
addRoute(pattern="/api/:format/",
     handler="api",
     action="execute",
     contraints={
      format = "(xml|json)"
     });

Optional Placeholders

Most of the time we need to create several routes in order to determine possible routings and they must be declared in a specific order so they match. A great example is the declaration of the following:

/blog/:year-numeric/:month-numeric/:day-numeric
/blog/:year-numeric/:month-numeric
/blog/:year-numeric/
/blog/

However, we just wrote 4 routes for this when we can just use optional variables by using the ? symbol at the end of the placeholder. This tells the processor to create the routes for you in the most detailed manner first:

/blog/:year-numeric?/:month-numeric?/:day-numeric?

Just remember that an optional placeholder cannot be followed by a non-optional one. It doesn't make sense.

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 list of name-value pairs that you can pass to the route definition. This basically tells the processor that if that specific route matches, then add those name-value pairs to the request collection.

addRoute(pattern="space/:space",handler="page",action="show",matchVariables="spaceUsed=true,useNavigation=false");

As mentioned above, you can also add variables by just adding them as arguments to the addRoute() method:

addRoute(pattern="space/:space",handler="page",action="show",spaceUsed=true,useNavigation=false);

URL Mapping Namespaces

You can create a-la-carte namespaces for URL routes. Namespaces are cool groupings of routes according to a specific URL entry point. So you can say that all URLs that start with /testing will be found in the testing namespace and it will iterate through the namespace routes until it matches one of them. Much how modules work, where you have a module entry point, now you can create virtual entry point to ANY route by namespacing it. This route can be a module a non-module, package, or whatever you like. You start off by registering the namespace using the addNamespace(pattern, namespace) method:

addNamespace(pattern="/testing", namespace="test");
addNamespace(pattern="/news", namespace="blog");

Once a namespace is registered you can add routes to it via the addRoute() method or the with() closure.

// Via addRoute
addNamespace(pattern="/news", namespace="blog")
  .addRoute(pattern="/",handler="blog",action="index",namespace="blog")
  .addRoute(pattern="/:year-numeric?/:month-numeric?/:day-numeric?",handler="blog",action="index",namespace="blog");

// Via with closure
addNamespace(pattern="/news", namespace="blog");
with(namespace="blog", handler="blog")
  .addRoute(pattern="/",action="index")
  .addRoute(pattern="/:year-numeric?/:month-numeric?/:day-numeric?",action="index");
.endWith();

You can also register multiple URL patterns that point to the same namespace

Module Routes

You can use the addModuleRoutes() to define a-la-carte module entry points. By convention, all module's declared entry point in their configuration file is registered for you. However, you can use this manual method approach to create aliases or funky stuff:

addModuleRoutes(pattern="/blog", module="blog");
addModuleRoutes(pattern="/news", module="blog");

With Closures

We have created some cool context methods to allow for the prefixing of any of the addRoute() arguments by using what we call with closures. This allows you to prefix repetitive patterns in route declarations. The best way to see how it works is by example:

addRoute(pattern="/news", handler="public.news", action="index");
addRoute(pattern="/news/recent", handler="public.news", action="recent");
addRoute(pattern="/news/removed", handler="public.news", action="removed");
addRoute(pattern="/news/add/:title", handler="public.news", action="add");
addRoute(pattern="/news/delete/:slug", handler="public.news", action="remove");
addRoute(pattern="/news/v/:slug", handler="public.news", action="view");

As you can see from the routes above, we have lots of repetitive code that we can clean out. So let's look at the same routes but using some nice with closures.

with(pattern="/news", handler="public.news")
  .addRoute(pattern="/", action="index")
  .addRoute(pattern="/recent", action="recent")
  .addRoute(pattern="/removed", action="removed")
  .addRoute(pattern="/add/:title",  action="add")
  .addRoute(pattern="/delete/:slug", action="remove")
  .addRoute(pattern="/v/:slug", action="view")
.endWith();

As you can see, we start our URL mapping DSL with the with() method and pass in any argument the addRoute() method declares. In this case we pass a pattern and a handler. Meaning any addRoute() method that is concatenated in the with closure will be prefixed with that pattern and handler. Once we concatenate the last addRoute(), then we finalize the closure with the .endWith(); demarcation. BOOM! The patterns look so much manageable and declarable. The arguments you can use for prefixing or defaulting are:

Argument Description
pattern The URL pattern prefix
handler The handler prefix
action The action prefix
matchVariables The match variables prefix
view The view prefix
constraints The constraints to prefix
module The module prefix
namespace The namespace prefix

Important : It is extremely important that you close the with closures with an endWith() call or all subsequent addRoutes() calls, will be using the last with closure you declared.

PathInfo Providers

By default, the URL mapping processor will detect routes by looking at the CGI.PATH_INFO variable. This feature can be useful to set flags for each request based on a URL and then clean or parse the URL to a more generic form to allow for simple route declarations. Uses may include internationalization (i18n) and supporting multiple experiences based on devices such as Desktop, Tablet, Mobile and TV. To modify the URI used by the SES interceptor before route detection occurs simple follow the convention of adding a UDF called PathInfoProvider() to your routes configuration file (config/Routes.cfm).

The PathInfoProvider() UDF is responsibile for returning the string used to match a route. Without this UDF the SES interceptor will simply obtain the URI from the CGI.PATH_INFO variable.

// Example PathInfoProvider for detecting a mobile request
function PathInfoProvider(Event){
  var rc = Event.getCollection();
  var prc = Event.getCollection(private=true);
  
  local.URI = CGI.PATH_INFO;
  
  if (reFindNoCase('^/m',local.URI) == 0)
  {
    // Does not look like this could be a mobile request...
    return local.URI;
  }
  
  // Mobile Request? Let's find out.
  
  // If the URI is "/m" it is easy to determine that this is a
  // request for the Mobile Homepage.
  if (len(local.URI) == 2)
  {
    prc.mobile = true;
    // Simply return "/" since they want the mobile homepage
    return "/";
  }
  
  // Only continue with our mobile evaluation if we have a slash after
  // our "/m". Without a slash following the /m the route is something
  // else like coldbox.org/makes/cool/stuff
  if (REFindNoCase('^/m/',local.URI) == 1)
  {
    // Looks like we are mobile!
    prc.mobile = true;

    // Remove our "/m/" determination and continue
    // processing for languages...
    local.URI = REReplaceNoCase(local.URI,'^/m/','/');
  }
  
  // The URI starts with an "m" but does not look like
  // a mobile request. So, simply return the URI for normal
  // route detection...
  return local.URI;
} 

event.buildLink()

public any buildLink(string linkto, [boolean translate='true'], [boolean ssl='false'], [string baseURL=''], [string queryString=''])

The request context has a method called buildLink() that will build SES URLs for you. Just pass in the route or event and it will create the appropriate URL for you:

<a href="#event.buildLink('home.about')#">About</a>
<a href="#event.buildLink('user.edit.id.#user.getID()#')#">Edit User</a>

The buildlink will translate any periods (.) to (/) slashes for you automatically

HTML 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')#">

This tells the browsers how to find your assets when using URL mappings or SES URLs.