How to Maintain Simple, Static Pages in CakePHP

Cake’s default way of handling simple static content is to use the built-in PagesController to serve up .tpl files from /app/views/pages. This is a simple and straightforward approach and works for very small websites, but comes with some obvious drawbacks:

  • Making changes to the content of the pages requires editing template files;
  • There’s no easy way (generally) to edit these pages the way you’d edit other content on your site, using controllers with admin actions, for example;
  • There’s no way to specify a hierarchy of pages, which can be quite useful for large websites;
  • The URL structure of the pages, although it follows Cake’s URL conventions, isn’t intuitive and looks pretty clunky and unprofessional. It would be much nicer to have /about rather than /pages/about, and so forth.

Logically, it makes more sense to place static content in the database so that it can be manipulated just like any other model. Additionally, we want our URLs to be pretty.

This is surprisingly easy to accomplish, thanks in part to this article, which shows how to load a model from within our routes configuration. I use the same technique, but my code below is updated to run on CakePHP 1.2, and I’ll show you how to create a hierarchy of nested pages as well.

Database

Let’s start by creating the database schema for our model. I call mine “StaticPage”, but you can name yours whatever you’d like. I don’t use “Page” to avoid controller conflicts down the road should I ever decide to make use of Cake’s PagesController for anything.

CREATE TABLE IF NOT EXISTS `static_pages` (
  `id` INTEGER(12) NOT NULL AUTO_INCREMENT,
  `parent_id` INTEGER(12) NULL,
  `title` VARCHAR(128) NOT NULL,
  `slug` VARCHAR(128) NOT NULL,
  `content` LONGTEXT NOT NULL,
  PRIMARY_KEY(`id`)
);

Model

Next, let’s create the actual model in CakePHP. We need to define the belongsTo and hasMany relationships for the tree hierarchy to work properly:

<?php
class StaticPage extends AppModel {

  var $belongsTo = array(
    'ParentPage' => array(
      'className' => 'StaticPage',
      'foreignKey' => 'parent_id'
  ));

  var $hasMany = array(
    'ChildPage' => array(
      'className' => 'StaticPage',
      'foreignKey' => 'parent_id',
      'dependent' => false
  ));
}
?>

Controller

Moving on, we create our StaticPagesController. You’ll likely want to create your own admin CRUD actions (the entire point of this, afterall, is to be able to manage these pages dynamically!), but for simplicity I’m just going to define the one action we need to display our pages. Conveniently, we’re just going to use the “index” action:

<?php
function index( $slug = null ) {
  if (!$slug) {
    $this->Session->setFlash(__('Invalid StaticPage.', true));
    $this->redirect(array('action'=>'index'));
  }
  $staticPage = $this->StaticPage->find('first', array(
    'conditions' => array(
      'StaticPage.slug' => $slug
  )));
  $this->set(compact('staticPage'));
  $this->pageTitle = $staticPage['StaticPage']['title'];
}
?>

Notice we’re going to be using the slug as the unique identifier when looking up the page. But what happens if we have two pages with the same slug? As you’ll see, that won’t be a problem as long as they’re nested under separate parent pages.

Routing

Now for the key part: we need to set up a custom route to handle our pages. In our routes.php file, we’re going to pull a list of static page slugs from the database and use them as the regular expression to match against with our route:

<?php
// routes.php

App::import('Model', 'StaticPage');
$page = new StaticPage();
$slugs = $page->find('list', array(
  'fields' => array('StaticPage.slug'),
  'order' => 'StaticPage.slug DESC'
));

Router::connect('/:slug/',
  array('controller' => 'static_pages', 'action' => 'index'),
  array(
    'pass' => array('slug'),
    'slug' => implode($slugs, '|')
));
?>

Now that everything is set up, we can start creating some static pages in the database. The key thing to remember is that the slug should be the full path to the page. So, for example, if we create a page called “about”, the slug should simply be “about”. The page will then be accessible at yourdomain.com/about. If we want to create a subpage of that page called “projects”, the slug for that page should be “about/projects”. Some people may not like storing the full path as the slug, but I find that it has two main advantages: it prevents ambiguity among pages with the same name/slug, and it makes managing your pages easier since you can immediately know the location of any given page.

This is also the reason that we load the slugs from the database in decending order for our regular expression: the route can first try to match the full URL of a subpage before the parent page is considered for matching.

If you want to take this one step further, you could write some sort of method, getFullSlug(), for the StaticPage model that generates the full slug (rather that storing it) by recursively appending the simple slug from parent pages. The obvious downside to this is that more SQL queries will be required, something we want to avoid, especially when dealing with what should be static content.

Happy baking!

    • Matt
    • July 6th, 2009

    Hi, thanks for this tutorial.

    I have used this as the basis for a small project I am working on. I have noticed that after I import the StaticPage model in bootstrap.php in the StaticPage controller the StaticPage model loses any behaviours that are set in it’s class file. I’ve tried to use the code below instead as a way of importing the Static Page model:

    App::import(‘Core’,'ClassRegistry’);
    $post = ClassRegistry::init(‘StaticPage’);

    It doesn’t seem to make any difference though and in StaticPage Controller I have to resort to using:

    public function beforeFilter()
    {
    if(!$this->Post->Behaviors->enabled(‘Tree’))
    {
    $this->Post->Behaviors->attach(‘Tree’, array(‘left’ => ‘left’, ‘right’ => ‘right’));
    $this->Post->Behaviors->enable(‘Tree’);
    }
    }

    It’d be great if you have any suggestions on how to get round this problem.

    Thanks again,

    Matt

  1. @Matt: I have very limited experience with behaviors in Cake; I’ve rarely used them in the past. I don’t presently know of a workaround, but if I get the chance to try and reproduce the problem I’ll see if I can find one.

    • murraybiscuit
    • September 11th, 2009

    thanks for this article. i’ve recently started working in cake and having some teething problems. particularly with this issue. i like this approach – it’s very similar to a cms i’ve built before.

    • murraybiscuit
    • September 11th, 2009

    i would also add seo keywords and description to the static_pages table.

    • Matt
    • September 27th, 2009

    Thanks for clearing this out, this is as you also have seen very helpfull.

    Are you willing to add the pagenames and paths to above the pagecode ? This would be nice for a lot of people I think !

    Thanks a lot.

  2. Hey Jeff, there’s no need to code a custom controller for static pages if you want a hierarchy. The pages_controller supports this feature. As far as making the content dynamic, that’s a fair enough reason.

    You also claim that the URLs look unprofessional, and yet you use the Router in your code. Somewhat confusing. Router would solve your ugly URL problem. Pages wasn’t designed for dynamic content, so any bashing of it therein is somewhat irrelivant.

    Components are a far simpler way to allow updateable content on a static template.

    • Dam
    • October 28th, 2009

    Hi ,

    How can i show the controller page, with out giving the DB info int he database.php

    means i need to run the controller mypage method photos

    http://www.example.com/mypage/photos

    without giving Db info how can i run

  1. No trackbacks yet.