Build your own CMS – Iteration 1: Data Model, Templates, Content Creation

January 29, 2011

This is the fourth post in a series documenting my development and implementation of a content management system (CMS) built on the ASP.NET MVC Platform.

This content management system will be built iteratively. Iteration 1 is far from a complete solution, but it is already fully functioning as a CMS. This post will discuss the current (simple!) database architecture and the concept of (straightforward!) templating. Then I’ll move on to how the admin site (dynamically!) provides a way to create and edit content pages.

The database

It doesn’t get any simpler than this. If you exclude the Languages table which is only there to support the future localization features of the CMS, we’re only working with two tables.

CMS database

ContentPages – this contains all content “page” records, which currently consists of the page’s URL alias, and the XML-based template name.

ContentPages table

ContentItems – this contains the actual content associated with a page. Each content record is for a specific language and contentid. The contentid is the identifier of a “content item” element in the XML template file. More on that below.

 ContentItems table

Languages – this should be self-explanatory. I’ll be discussing this in depth in future posts. For now just relax, everything is going to have a language id of “1” – English.

Template-driven content

XML-based templates determine the set of “content items” that defines a type of page. Below is the Product template:

<?xml version="1.0" encoding="utf-8" ?>
<template name="Product">
    <contentItems>
        <include template="_Includes\PageMaster.xml" />
        <content id="Title" type="text" isHtml="false"></content>
        <content id="Body" type="textarea" isHtml="true"></content>
        <content id="PrimaryImageUrl" type="text" isHtml="false"></content>
    </contentItems>
</template>

The id attributes define the unique id of the content item and this corresponds to the contentid column in the ContentItems database table. The other attributes are metadata that describes what type of content item it is. These are used in the CMS Administration application to render and validate the proper field type (when creating/editing a page), and by the client web site to render the content.

Templates support nesting, which provides the flexibility to define content items that may be used by multiple templates. The <include> element tells the system to pull in the content items from the included template.

The PageMaster template consists of a few globally used content items:

<?xml version="1.0" encoding="utf-8" ?>
<template name="PageMaster">
    <contentItems>
        <content id="PageTitle" type="text" isHtml="false"></content>
        <content id="MetaKeywords" type="text" isHtml="false"></content>
        <content id="MetaDescription" type="text" isHtml="false"></content>
    </contentItems>
</template>

This means a Product page consists of 6 total content elements: PageTitle, MetaKeywords, MetaDescription, Title, Body, and PrimaryImageUrl.

The CMS Administration Application

Oh yes, I brought a chart.

CMS architecture

This is mostly marketing BS – an attempt to fool you into thinking I know what I’m doing. Okay, I’m only half-kidding. The reality is that some of the lines between components here are blurred and, as this is just iteration 1, it will likely change. The basic idea is this:

(MVC App + Template Files + Data) * Awesome = CMS

Below is an example of the “Create Content Page” page. Select a template, select a language, and the Page Content items are loaded up and dynamically* a form gets rendered. Select a URL alias for the page, fill out any content fields, save – done!

Create Content Pag

*Rendering this form dynamically is a double-edged sword. It’s great because after coding this just once, end users can create and edit an infinite number of pages with an infinite variety of templates. However, with many fields this form could get a bit unwieldy. Users might have trouble finding the field that corresponds to the part of the rendered page they are attempting to edit. Therefore, I’m also going to build an override system where a developer can explicitly design and layout the fields of a particular template in any way they see fit. A double-edged sword of awesome.

I’m just going to post pseudo-code, as it would be a bit much to include in full. The form gets rendered in a spirit similar to Dynamic forms with ASP.NET MVC and XML. The Controller retrieves the template and passes it to the view, which in part might look something like this:

<div>
    <fieldset>
       <legend>Page Content</legend>

            <%foreach(var item in Model.Template.ContentItems) { %>
                <div class="editor-label">
                    <%=item.Id %>
                </div>
                <div class="editor-field">
                    <!-- hiding logic to select the proper input type/style 
			    based on attributes of ContentItem -->
                    <%=Html.TextBox(item.Id, item.value) %>
                </div>
            <%} %>

            <input type="hidden" name="templateName" value="<%=Model.Template.Name %>" />
            <input type="submit" name="submit" value="Create Page" />

    </fieldset>
</div>

The form is posted to a controller that does something along the lines of:

[HttpPost]
[ValidateInput(false)]
public ActionResult CreateNewPage(string alias, string templateName /*, form data */)
{
    var template = TemplateManager.GetTemplate(templateName);

    var contentItems = MapFormFieldsToTemplateAndCreateListOfContentItems();

    _repository.CreateContentItems(contentItems);

    return RedirectToAction("success");
}

Once this is successfully saved to the database, the data looks something like this:

image

The page edit mechanism works the same way, only with the added step of having to query the content for a given contentpageid. With the magic of the (not) patent-pending “Template Transformation Engine”, the data is wired up with the template into a nice moist ball of content goodness.

//get page by id
var contentPage = _repo.ContentPages.Query
    .Where(x => x.Id == theContentPageIdToEdit)
    .SingleOrDefault();

//get the template metadata by template name (xml file lookup)
var template = TemplateManager.GetTemplate(contentPage.TemplateName);

//loop through each item in template, find matching item within the page content
foreach (var templateContentItem in Template.ContentItems)
{
    var contentItem = contentPage.ContentItems
        .Where(c => c.ContentId == templateContentItem.Id)
        .SingleOrDefault();

    //templateContentItem & contentItem is everything we need to
    //render the hydrated and formatted form field for this piece of content
}

And this brings us to the next discussion: how the client website for our fictitious company, GlobalBeers, retrieves and renders this content on its pages. Stay tuned!

URL Aliasing: serving data-driven pages with custom URLs

January 28, 2011

This is the third post in a series documenting my development and implementation of a content management system (CMS) built on the ASP.NET MVC Platform.

URL aliasing, or URL rewriting, is a core feature of this application. Content creators need the ability to create new pages with a URL of their choice and it just needs to work. We can’t have the developer involved and managing a complex and always changing set of page URLs.

Thus, the URL of CMS content pages will come from the database like the rest of the content.

Initial data model for the “ContentPages” table

ContentPages table

This is plain ole vanilla ASP.NET URL rewriting with an HttpModule. Other than the fact we’re also managing the hand-off of the actual CMS page content object at the same time, the implementation is not unique.

URL Aliasing module flow

Aliasing with HttpContext.RewritePath()

In order for the application to serve up pages with arbitrary, data-driven URLs, we need to detect them and rewrite the request path to a generic Controller Action that can serve up the page.

I’ve done this with an HttpModule on BeginRequest:

void context_BeginRequest(object sender, EventArgs e)
{
    var app = sender as HttpApplication;
    if (app == null) return;

    var repo = ObjectFactory.GetInstance<ICmsRepository>();

    //try to get a CMS content page with alias matching current request
    var contentPage = repo.ContentPages.Query
        .Where(x => x.Alias == app.Request.FilePath)
        .SingleOrDefault();

    //if not found, request isn't for a CMS page. abort
    if (contentPage == null) return; 

    //request is for a CMS page. Add content object to request
    //and execute generic Controller Action
    app.Context.Items.Add("contentPage", contentPage);
    app.Context.RewritePath("~/Cms/ContentPage");
}

This module is querying the database for a CMS page record that has claimed ownership of the requested URL. If no record is found, the module returns (having done nothing) and the request falls down the pipeline, where a native ASP.NET MVC Controller Action will handle the request (or something else will happen, or a 404, etc).

If a record is found, it means we’re dealing with a CMS page. I’m adding the contentPage object to the request Context.Items collection so it can be retrieved later, and then calling RewritePath(), telling ASP.NET to instead execute the ContentPage Action of my Cms Controller.

public ActionResult ContentPage()
{
    var contentPage = HttpContext.Items["contentPage"] as ContentPage;
    if (contentPage == null) 
        throw new InvalidOperationException("Content Page missing.");

    return View(contentPage.TemplateName, contentPage);
}

Above is the all-purpose ContentPage Controller Action that will execute every CMS page request. All it does at its core is pull out the contentPage object from the Context.Items collection (where the HttpModule put it), and renders the view.

By convention, all page templates must have a view page of the same name, so the appropriate view is rendered.

Build your own CMS – an overview

January 27, 2011

This is the second post in a series documenting my development and implementation of a content management system (CMS) built on the ASP.NET MVC Platform.

In this post, we’ll review the existing website of the client and the rough CMS solution architecture.

Introducing GlobalBeers

My client is the fictitious internet company, GlobalBeers, that markets and sells beer from all over the world, to all over the world. They currently have a website consisting of mostly static pages which will need to be replaced or enhanced by the CMS I’m building.

GlobalBeers website GlobalBeers website

The site has the following page hierarchy:

  • Home
  • Products (lists product categories and 2 featured products)
    • Product Category (describes the category and lists all products within)
      • Product Page (product page of a specific beer)
  • Company
    • About
    • Contact

GlobalBeers website GlobalBeers website

All pages sit in a standard master page template that renders the header, footer and menu. The menu lists all of the pages except for the individual product pages.

Solution Architecture

During development I’m going to put the existing GlobalBeers website (as described above) in the same solution as the CMS projects, but this is not necessary.

The CMS platform itself will consist of two projects:

  • A class library containing all core CMS code
  • An ASP.NET MVC CMS administration web application

Once CMSified, the GlobalBeers website will have to contain a reference to the core CMS class library, but not the administrative MVC site – that’ll be isolated so it can be deployed and administered separately (for instance, on an internal, private network).

Rough solution architecture: (I’m showing only what’s interesting)

  • GlobalBeers.Cms  [class library]
    • Models
    • Repositories
    • Templates
      • _Includes
      • Default.xml
    • Web
      • Controllers
        • CmsController.cs
      • ViewModels
        • CmsPageViewModelBase.cs
      • Modules
        • CmsPageModule.cs
        • LanguageModule.cs
  • GlobalBeers.Cms.WebUI [administrative CMS site]
    • Content
    • Controllers
      • AdminController.cs
    • Scripts
    • Views
      • Admin
  • GlobalBeers.WebUI [existing GlobalBeers website]

To be clear, there are two web applications. The administrative CMS website is the place end users will log into and create, edit and configure content that is ultimately all saved to a database. The client website is the one that actually contains the true front end – the fictitious www.globalbeers.com.

I’ll get into the details of the CMS projects and some of the code I’ve included here in the following posts.

Technology choices

I’ll be using ASP.NET MVC 2 and .NET 4.

Why MVC and not Web Forms?

I debated building the CMS platform with ASP.NET Webforms, but it was clear that MVC was best choice. Pages will be heavily based on templating which will require an immense amount of flexibility in the way pages are constructed and rendered.

I blogged a while back about XML-driven dynamic forms and how it’s such a relief not having to deal with the complex control hierarchy of the Webforms model. This CMS platform will use XML templates and render certain administrative pages in a similar way.

What about the client’s existing website?

This is running MVC too, but only for convenience. It is actually my intent for this CMS platform to work with Webforms as well. One of the core requirements of mine is to not impose a strict set of technology standards or development methodologies on the consuming website.

To utilize the CMS, the client website will only need to:

  • Reference the required DLLs
  • Configure a few Http Modules in the web.config

More to come!

Building a content management system with ASP.NET MVC

January 23, 2011

This is the introductory post in a series documenting my development and implementation of a content management system (CMS) built on the ASP.NET MVC platform.

Background

At my day job I administer and support a content management system that sucks. It’s not all the vendor’s fault. Like any “use this for all of your needs” software package, we’re buried by the weight of countless features we never wanted and will never use. On top of this, we’re stuck following the technology and architectural directions of the platform which limits our flexibility (and in most cases, is counter to our preferences).

What do I want? I just want a system that allows content editors to edit copy on the website without having to call up an expensive developer to edit the typo on the “about us” page. That’s all.

Feature-less, not feature-rich.

Restrictive, but based on our chosen restrictions, not a vendor’s.

Light-weight, clean, simple.

So I’m going to build one

I’m in over my head, I know. CMS platforms are hard. There’s a reason everyone hates them. But over the next several months, I’m going to build one, use it on my own website, and blog about various features and aspects of the system.

It won’t be for you. It will be based entirely on my decisions and direction, and from the requirements provided to me from my fictitious customer - who will also be played by me :).

High-level notes and requirements

I’m planning to support the following features (more to be determined later):

  • Multi-lingual support
  • Url aliasing
  • Can be used with Webforms (websites and web apps) or MVC

As I mentioned, this will be a restrictive system. This isn’t a “build your own website” platform that can be created and administered by an end user alone.

  • HTML, design, template creation, layout will all be done by the developer
  • Textual copy and light formatting (bold, italics, etc) will be managed by the end user

This means that end users will have the ability to edit textual content of existing CMS-based pages and to create new pages off existing templates.  New templates, new content areas, or new page layout/design will still require the involvement of a developer.

Posts

  1. this post
  2. Build your own CMS – an overview
  3. URL aliasing – serving data-driven pages with custom URLs
  4. Iteration 1: Data Model, Templates, Content Creation

About Kurt

I'm a senior consultant at Headspring in Austin, TX. My passion is creating web-based applications that are well crafted and solve real problems for real people. Want to know more? Check out my about page.

. @LipGlosserie setting up for Renegade Austin craft fair http://t.co/7X4WBVQb 15 hours ago