Very simple, very ugly, CMS built with ColdFusion 9

This post is more than 2 years old.

Earlier on in the ColdFusion 9 beta, I worked on a simple CMS (Content Management System) that made use of ORM. I've messed with it every now and then over the past few months and spent some time today making it a bit nicer so I could share the code with others. I may, stress may turn this into a real project, but I have no real intention of trying to compete with Mura or Farcry. This was just for fun, and just to get some experience with ORM. Let me also add that as it's been worked on for a few months now, you may see some code that doesn't quite make sense. So for example, earlier on in the ColdFusion 9 alpha, there was no isNull. Today I replaced code like this:

<cfset section=EntityLoad('section', url.delete, true)> <cfif isDefined("section")>

with

<cfset section=EntityLoad('section', url.delete, true)> <cfif not isNull(section)>

You may see stuff like that in the code, so please keep in mind that this isn't "best practice" ColdFusion 9 code. With that out of the way, let me talk a bit about the architecture.

Simple CMS works with a simple model. The model is so simple I'm going to paste it all here. First is our template object:

component persistent="true" {
property name="id" generator="native" sqltype="integer" fieldtype="id";
property name="name" ormtype="string";
property name="header" ormtype="text";
property name="footer" ormtype="text";

}

Templates consist of an ID, a name, and a header and a footer. Next up is the section:

component persistent="true" {
property name="id" generator="native" sqltype="integer" fieldtype="id";
property name="name" ormtype="string";
property name="sitedefault" ormtype="boolean";
property name="order" ormtype="integer";

}

Sections consist of a name, a sitedefault property, and an order. The sitedefault property is simply a way to mark a section as the default section of a web site. Like a home page section for example. Order is used for ordering sections for display. More on that later. The last part of our model is the page:

component persistent="true" {
property name="id" generator="native" sqltype="integer" fieldtype="id";
property name="title" ormtype="string";
property name="body" ormtype="text";

property name="section" fieldType="many-to-one" cfc="section" fkcolumn="sectionidfk";
property name="template" fieldType="many-to-one" cfc="template" fkcolumn="templateidfk";

property name="isHomePage" datatype="boolean";

public string function renderMe() {
	return template.getHeader() & body & template.getFooter();
}

}

Finally - a bit of complexity! Pages consist of a title, a body, and a related section and template. Lastly - a isHomePage property works much like the section siteDefault property. It is a way to say "if you request a section without a page, this is the one to load." Oh, and I've got a method to render the page. As you can see, it gets the template and wraps the body.

So how does the CMS work? There is one more CFC called cms. This component acts as a main controller for all CMS actions. When you come to the application with nothing in the URL (but the path to the application), it tries to find a default section and a page marked as a home page for that section. The application will nicely handle the lack of either of these values by showing a simple message. If you go to the application with a path in the URL: /cmsalpha/products/index.cfm, then it looks for a section named products and a home page object. Lastly, if you go to /cmsalpha/products/foo.cfm, it will look for a page named foo inside the products section.

The application makes use of onMissingMethod in Application.cfc to handle requests. Unfortunately, this means you can't do: /cmsalpha/products/. You must supply a full path like so: /cmsalpha/products/index.cfm. But that's a small trade off for a simple proof of concept. (You could always use a server side rewriter to handle this too.) Anyway, here is th ecode from Application.cfc:

public boolean function onMissingTemplate(string pageRequested) {
try {
	var page = application.cms.getPage(arguments.pageRequested);
} catch(any e) {
	//If the error code is 1, it's reflects the lack of a default section, which we handle nicely
	if(e.errorCode == 1) location("#application.cms.getCMSURL()#/notready.cfm");
		//not safe to assume .., will fix later
	if(e.errorCode == 2) location("#application.cms.getCMSURL()#/404.cfm?msg=#urlEncodedFormat(e.message)#");
	writeDump(e);
	abort;
}

application.cms.renderContent(page);
	
return true;

}

Obviously there is an admin as well. The admin let's you edit pages, sections, and templates. What's cool is - if you try to create a page with no templates or sections in the database, it will notice this and stop you. The admin is currently unprotected. I've added it to my list of things to add later on (see final notes).

There is one really cool part to this (imho). When the application renders a page, it does it via the VFS:

public string function renderContent(page) { var result = arguments.page.renderMe(); var vfile = hash(arguments.page.getSection().getName() & "/" & arguments.page.getTitle()); var vpath = expandPath("/vfs") & "/" & vfile; fileWrite(vpath,result); //before we run, copy some variables over so we can have dynamic templates local.title = page.getTitle(); local.section = page.getSection().getName(); local.sectionlist = getSectionList(); local.cmsurl = getCMSURL(); writeLog(file="cms",text="local.cmsurl=#local.cmsurl#"); include "/vfs/#vfile#"; }

What this means is that you can include code in your templates. For example, my main template footer has:

<p align="center"> <cfoutput>Copyright #year(now())#</cfoutput> </p>

And it works! Also - do you see all those local.* variables? I pass in a bunch of variables into the local scope so that both templates and pages can make use of the variables. So for example, check out the header:

<html>

<head> <cfoutput><title>#local.section# / #local.title#</title></cfoutput> </head>

<body bgcolor="green">

<table width="80%" bgcolor="white"> <tr> <td align="center"> <cfloop index="l" list="#local.sectionlist#"> <cfoutput><a href="#local.cmsurl#/#l#/index.cfm">#l#</a> <cfif l is not listLast(local.sectionList)>/</cfif></cfoutput> </cfloop> </td> </tr> <tr> <td> <cfoutput><h1>#local.section# / #local.title#</h1></cfoutput>

As you can see, I make use of the section name and page title in the title tag. Also note sectionlist. Remember when I said we had an order for sections? This comes into play here as it lets me spit out a simple ordered menu:

Of course, letting users write code like that in the admin is something of a security risk. You could use tokens instead (%title% would be a page title), but it's kinda cool how well it works.

Anyway, you can download the demo below. You will want to edit these lines in Application.cfc to meet your system requirements:

this.datasource="cms1"; this.ormsettings = { dialect="MySQL", dbcreate="update" };

You just need to change datasource and dialect. Oh, and I freaking love the fact that I didn't have to make a table once. Oh, and I freaking love that I added 'order' to section in the CFC, reloaded, and bam, the column was added for me. Me love me some ORM.

p.s. One more thing I'd like to do with this application later on. Hibernate (and CF9's use of it) allows you to run code on various events. In theory, I should be able to write code that says, "If I save a section and mark it as section default, update all other sections and set that value to false." I haven't played with events yet so that will be my next experiment.

Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Sean Coyne posted on 7/26/2009 at 2:21 AM

It would be cool to see how you could use hibernates event model and centaur's new cache functions to cache pages the when changes are made in the admin, replace the cached value with the newly edited page. Very cool stuff.

Comment 2 by Raymond Camden posted on 7/26/2009 at 5:17 AM

Yep, I could envision a 'cached' value on both pages and sections allowing you to cache at either level.

Comment 3 by Misty posted on 7/26/2009 at 7:09 AM

Hi Ray! Where i can see what are the new tags which are introduced in CF9. Also one Request can u tell me any link on website where i can see how i can run Coldfusion 8 & Railo 3 on Same Machine

Comment 4 by Raymond Camden posted on 7/26/2009 at 7:12 AM

If you go to the Labs page:

http://labs.adobe.com/techn...

Click on the community tag, you will see links to docs.

Comment 5 by Johan posted on 7/27/2009 at 8:26 AM

Interesting! I tested using onMissingTemplate() and found it quite slow so would not recommend it for a solution that takes lots of traffic.

Comment 6 by Jody Fitzpatrick posted on 7/27/2009 at 5:04 PM

Hey Ray

I'm currently working on a CMS system that I hope to be great. I want to release it to RIAForge while its in development but I don't want it to be available once production is finished.

But great post, and love the new blog once again.

Comment 7 by Raymond Camden posted on 7/27/2009 at 5:12 PM

Johan: I have not seen that myself. Can you share sone tests?

John: you can hold off uploading a file. I don't like that very much and recommend you don't hold off fir long, but u can. Pardon typos - on iPhone.

Comment 8 by Art Holland posted on 7/27/2009 at 8:22 PM

Ray, do you know if Adobe has/will enable one to create a Solr collection from the Hibernate ORM? It appears to have been done by the Hibernate community:
https://www.hibernate.org/4...

Thanks for sharing your CMS, it's very helpful to see good CF-ORM example.

Comment 9 by Raymond Camden posted on 7/28/2009 at 8:08 PM

Forgive typos, on iPhone. I am not aware of any specific support for this. I do know that you can get access to the XML file that configures your app's use of hibernate, but it isn't an area I've researched.

Comment 10 by Sahr Johnny posted on 7/31/2009 at 1:53 PM

Interesting Ray. We just went beta with a new Coldfusion-based CMS called I.C.E.
that integrates content management, campaign management, crm, API managements, rss syndication
and analytics. You are all welcome to take it for a test drive.

Comment 11 by Johan posted on 11/20/2009 at 3:31 AM

Sorry I missed your reply Ray - testing/slow onMissingTemplate() - it was sometime back when I tested using onMissingTemplate() with my CMS as a way to handle different URL formats. Requesting the same "real" url via my current method (index.cfm/xxx) and onMissingTemplate() using the MSFT web stress tool resulted is significantly slower response times. As a result I did not bother to investigate further. For me it also just feels wrong using onMissingTemplate() as a url rewriter.

Comment 12 by Raymond Camden posted on 11/20/2009 at 3:33 AM

Interesting. You can get away with it by using IIS/Apache side rewriting to always go to dyn.cfm?path=WHATYOURREQ here. Then your CF onMissingHandler isn't run - but a normal CFM.

Comment 13 by Kevin Roche posted on 6/30/2010 at 2:51 PM

Ray,

This is a really interesting exercise which has prompted a few ideas for some stuff I am working on.

I am having some trouble getting it to work as it should. In particular the code that is triggered to initialise the model (adding ?init=1 to the URL doesn't work for me). If I make changes to cms.cfc, it does not appear to update the cms object in memory.

I am using CF9 and IIS7. What am I doing wrong?

Kevin

Comment 14 by Raymond Camden posted on 6/30/2010 at 3:28 PM

Well, let's start simple. Does the hook in onRequestStart run? You should end up on a page with reloaded=true in the URL.

Comment 15 by Kevin Roche posted on 6/30/2010 at 4:57 PM

I doesn't. Which is the main reason why I know its not working.

Comment 16 by Raymond Camden posted on 6/30/2010 at 5:02 PM

Ok, so let's put some logging in there. Add a few writelogs to onRequestStart to see if you can find why it isn't running.

Comment 17 by Kevin Roche posted on 6/30/2010 at 6:23 PM

This is wierd. It never runs onRequestStart! Is it because this thing uses onMissingMethod ?

Comment 18 by Raymond Camden posted on 6/30/2010 at 6:24 PM

Inconceivable! ;) Seriously though - it should be automatic. Dumb question - but you are on CF9 right?

Comment 19 by Kevin Roche posted on 6/30/2010 at 6:26 PM

Yes if I add the missing template it run onRequestStart! Is that a bug in CF?

Comment 20 by Raymond Camden posted on 6/30/2010 at 6:29 PM

Wait a tick - when you added ?init=1 to the URL - were you also requesting a file that did not exist? like foobar.cfm?

Comment 21 by Kevin Roche posted on 6/30/2010 at 6:32 PM

Yes, because that is the way your app (above) works. It uses the path to work out what to get from the DB.

So it certainly looks like if you rely completely on onMissingTemplate as you did it won't ever run onRequestStart. I guess the workaround is to add a call to onRequestStart in onMissingTemplate.

Comment 22 by Raymond Camden posted on 6/30/2010 at 6:36 PM

Well hells bells. I had assumed I had at least an index.cfm. :) I wouldn't add a call to onRequestStart - I'd just move the logic into onMissingTemplate, since that is run 100% of the time then.

Or perhaps it needs to be an Admin only operation - with a link in the admin. That would work too. To be honest, that operation (the init like that) isn't really going to be used in production.