An alternative approach for static pages

Published on and tagged with cakephp  controller  ideas

A while ago Jonathan Snook showed in the article Easier static pages with CakePHP 1.2 how you can easily create static pages by creating view files without corresponding controller actions. For this purpose he wrote a custom error handler. The “problem” of this approach is that an error handler is meant to show error messages, and not static pages…

And so I looked for a different approach and ended up with a modified PagesController, combined with a route. The idea is to use the convention that each file in app/views/pages can be accessed like example.com/file (i.e. about.ctp is accessed as example.com/about and about/company.ctp as example.com/about/company).

Here is the PagesController:

// app/controllers/pages_controller.php
class PagesController extends AppController{
    public $uses = array();

    public function display() {
        $viewFilename = array_pop($this->params['pass']) . '.ctp';
        $viewFilename = str_replace('-', '_', $viewFilename);
		
        $viewPath =  DS . $viewFilename;
        if ($this->params['pass']) {
            $viewPath = DS . implode(DS, $this->params['pass']) . $viewPath;
        }
		
        if (file_exists(VIEWS.'pages'.$viewPath)) {
            $this->render(null, null, VIEWS.'pages'.$viewPath);
        } else {
            $this->cakeError('error404');
        }
    }
}

To get the aforementioned URL scheme we have to define a “catch all” route in app/config/routes.php:

Router::connect('*', array('controller' => 'pages', 'action' => 'display'));

This route has to be the latest route in the routes file. And that’s also the disadvantage of this approach: because of the “catch all” route you have to define routes for all controllers…

Anyway, I hope this approach is useful for some of you :)

28 comments baked

  • Khaled

    Great , very great …

    in the past I saw all the tutorials for doing so , but no one was suitable for me !! so I used to route every static page.

    so if you have 3 static pages you have to add 3 line in route file

    yeah it’s a bad way but I was just a beginner :D

    thanks again for this post

  • Richard@Home

    Nifty, but I have one question: What is wrong with cakes default pages controller? What is the problem you (and Jonathon) are trying to solve here?

  • Nate Todd

    @richard – It appears the real difference is that they want to avoid the “/pages” required in the default url scheme to display static pages through the pages controller without custom-defining each static route.

  • Richard@Home

    Then why not use some custom routes as recommended in the manual? I use the following code snippet to keep things sensible:

    // add the static pages
    $static_routes = array(
    “/about”=>”about”,
    “/contact”=>”contact”
    );

    foreach($static_routes as $url=>$page) {

    Router::connect($url, array(“controller” => “pages”, “action” => “display”, $page));

    }

  • cakebaker

    @all: Thanks for your comments!

    @Nate: That’s correct.

    @Richard: Your approach works fine if you have only a few static pages. But if the number of static pages grows, it is not that handy to add each page to the routes array. On the other hand, you could tweak your approach to scan the pages folder and to automatically define the respective routes. That would probably be a better solution than what I showed in the article ;-)

  • panther40k

    Hi,

    i hope i’m not completly wrong, because i’m still using cake 1.1. But i guess, this shoud be transferable to 1.2 as well.

    Because in the most times i have rather staic pages than controllers, i solve it in this way:

    $Route->connect(‘/myfunction/’, array(‘controller’ => ‘function’, ‘action’ => ‘index’));
    $Route->connect(‘/myfunction/*’, array(‘controller’ => ‘function’, ‘action’ => ‘view’));

    $Route->connect(‘(?!admin|function)(.*)’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’));

    $Route->connect(‘/’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘home’));

    All request will be redirect to the pages controller, except some urls, that points to a controller.
    These exceptions have to be defined before the pages route.

  • links for 2008-06-19 « Richard@Home

    […] An alternative approach for static pages – cakebaker I question the need for another pages controller… (tags: cakephp page controller static routes) […]

  • Andrew Allen

    I have liked your articles for quite a while but I disagree with your approach here. I like the idea that you don’t have to either create a route for each static page (yuck) or that you override all errors but I would like to propose a sort of combination approach. I haven’t actually tried it but I’ve been thinking about how to make it work for a little while and I think I have it down to only a few kinks. If you defined something like

    Router::connect(‘/real/:controller/:action/*’);
    Router::connect(‘*’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’));

    then in your pages/display use dispatcher to redispatch the request through the system (but now with the /real/ prefix) so that it would work. The only thing that I see not working (apart from maybe dispatching twice which I will admit would be slow) would be prefixes would have to be double prefixed to

    Router::connect(‘/real/admins/:controller/:action/*’, array(‘prefix’ => ‘admin’));

    Happy baking!

    Sincerely,
    ~Andrew Allen

  • John

    Before you generate the 404 you could check to see if(else if) the controller and action exists by checking if a file exists in the same way you looked for a page.

    Then you’d have a system that acts almost exactly the same by defined routes -> pages -> undefined controller/action

  • cakebaker

    @panther40k, Andrew, John: Thanks for your comments!

    @panther40k: Yes, that’s a very similar approach to what I described.

    @Andrew: Personally, I like it if people are critical, it’s usually a good opportunity to learn something.

    Your approach is interesting and should work fine. Regarding the performance I think it shouldn’t be an issue in most applications, unless you have a high-traffic site.

    @John: At least if you are using Andrew’s approach it is not necessary to check whether the controller and action exists, you simply call Dispatcher::dispatch(), and Cake does the rest (which also means you don’t have to generate the 404 yourself).

  • teknoid

    Interesting idea.

    I’ve been using a different, simple method by naming all static pages as *.html… (i.e. making all links on the site point to mysite.com/somePage.html instead of mysite.com/pages/somePage)

    Then all I need is one route, that doesn’t really interfere with anything else:
    Router::connect(’/(.*).html’, array(’controller’ => ‘pages’, ‘action’ => ‘display’));

  • cakebaker

    @teknoid: Simple but nice idea, thanks for sharing!

  • Felix Geisendörfer

    Overwrite AppError::error404 anybody? : )

  • cakebaker

    @Felix: Yes, that would probably work, but as I wrote in the article, I think you shouldn’t use AppError for anything else than error handling…

  • jason

    I used jonathan snook’s easier static pages and in addition added code that allowed me to get a page stored in the database. Just wondering if this would work with a database without a huge modification. Also, I noticed that jonathan snook’s method didn’t work when the debug is set to 0 in a production environment. Maybe that’s just my code and i screwed something up though..

  • cakebaker

    @jason: I think it shouldn’t be very difficult to use it with pages stored in the database. Define a model and use either $viewFilename or $viewPath to retrieve the respective page from the database. And then pass the page content to a simple view which outputs the page.

    And regarding the problem with Jonathan Snook’s method: you probably have to overwrite AppError::error404 as suggested by Felix, because if debug is set to 0 no missingXXX errors are shown to the user.

    Hope that helps!

  • Herod

    It has a small error, i think.
    I had to delete an DS from $viewPath to work.
    Maybe this will help someone.

    var $name = 'Pages';
    	
    	function display()
    	{
    		$viewFilename = array_pop($this->params['pass']) . ".ctp";
    		$viewFilename = str_replace('-', '_', $viewFilename);
    		$viewPath = DS . $viewFilename;
    		
    		if ($this->params['pass'])
    		{
    			$viewPath= implode(DS, $this->params['pass']) . $viewPath;
    		}
    		if (file_exists(VIEWS.$viewPath))
    		{
    			$this->render(null, null, VIEWS.$viewPath);
    		}
    		else
    		{
    			$this->cakeError('error404');
    		}
    	}
  • cakebaker

    @Herod: Thanks for your comment!

    Well, the DS is required so that $viewPath always starts with a DS (which is needed for the file_exists check I used in the article). But if you remove the “pages” from the file_exists call, then you are right, and the DS is not needed.

  • brian (headache)

    I like this technique very much. I do think it’s better than the AppError technique. I’ve had a very good system for a while now but wanted to move all my links to use routing arrays. Unfortunately, they always include that ‘pages’ bit. It doesn’t make sense to me that, if I have a route like this:

    Router::connect(‘/contact’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘contact’));

    … and I create a link like this:

    $html->link(
    ‘Contact’,
    array(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘contact’),
    array(‘title’ => ‘How to contact us’)
    )

    .. I get a link like “/pages/contact”. If I can use custom routes for other controllers, it should be possible here, also. Very strange.

    With your technique, using reverse routing is again out of the question, but it’s cleaner than what I’ve been using, so I think I’ll upgrade.

    I made a change to display() in order to have deeper levels without having to specify “index’ for the default page.

    eg. ‘/about’ or ‘/about/’ will display ‘views/pages/about/index.ctp’

    public function display()
    {
    $viewFilename = str_replace(‘-‘, ‘_’, array_pop($this->params[‘pass’]));

    $viewPath = DS . $viewFilename;
    if ($this->params[‘pass’])
    {
    $viewPath = DS . implode(DS, $this->params[‘pass’]) . $viewPath;
    }

    if (file_exists(VIEWS.’pages’.$viewPath.’.ctp’))
    {
    $this->render(null, null, VIEWS.’pages’.$viewPath . ‘.ctp’);
    }
    else if (file_exists(VIEWS.’pages’.$viewPath.DS.’index.ctp’))
    {
    $this->render(null, null, VIEWS.’pages’.$viewPath.DS.’index.ctp’);
    }
    else
    {
    $this->cakeError(‘error404’);
    }
    }

    I removed the ‘.ctp’ append to $viewPath so that it isn’t in the way in the “else if” test.

  • cakebaker

    @brian: Thanks for your comment!

    Which CakePHP version do you use? I just copied your route and your $html->link() statement into my test application (which uses the latest cake version from the repository), and there the reverse routing worked fine, i.e. the generated link points to “/contact”. Is it possible that you added your route after the default pages route (Router::connect(‘/pages/*’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’));)?

  • Arne Diekmann

    Hey!

    Thanks for the snippet, its very elegant! The only problem for me was the fact that I made heavy use of the theme-option in cakephp. 3 more lines and thats working also though:

    <?php 
    
    class PagesController extends AppController
    {
    
        public $uses = array();
    
        public function display()
        {
    
            $view_filename = array_pop($this->params['pass']) . '.ctp';
            $view_filename = str_replace('-', '_', $view_filename);
            $view_path =  DS . $view_filename;
    
            if ($this->params['pass'])
            {
    
                $view_path = DS . implode(DS, $this->params['pass']) . $view_path;
    
            }
    
            if (isset($this->theme) && file_exists(VIEWS . 'themed' . DS . $this->theme . DS . 'pages' . $view_path))
            {
    
                $this->render(null, null, VIEWS . 'themed' . DS . $this->theme . DS . 'pages' . $view_path);
    
            }
            elseif (file_exists(VIEWS . 'pages' . $view_path))
            {
    
                $this->render(null, null, VIEWS . 'pages' . $view_path);
    
            }
            else
            {
    
                $this->cakeError('error404');
    
            }
    
        }
    
    }
    
    ?>

    Greetings!

  • cakebaker

    @Arne: Thanks for this addition!

  • John

    Hi All

    I’ve been using the variant below (first posted here by panther40k I think) (different controller / action obviously)
    $Route->connect(’(?!admin|function)(.*)’, array(’controller’ => ‘nodes’, ‘action’ => ‘view’));
    for ages in 1.2 without any problems… until now. The problem is a bit long winded and is described on groups here: http://groups.google.com/group/cake-php/browse_thread/thread/9e295aefeeeb3eb8?hl=en

    Anyway what I am trying to do is rewrite the above route in a proper Cake 1.2 way e.g.

    Router::connect(
    '/*',
    array(
    'controller' => 'nodes',
    'action' => 'view',

    ),
    array('*' => '(?!admin|products)')
    );

    But it doesn't quite work - so /products/options/146 is still getting routed to /nodes/view

    What I am trying to do is have * parsed as a named parameter. Any ideas anyone?

  • cakebaker

    @John: Hm, I don’t know whether it is possible to write this as a single route. As a workaround I would simply define two additional routes for those routes you don’t want to handle with the “catch all” route.

    Maybe it is worth an enhancement ticket?

    Hope that helps!

  • John

    @cakebaker – I am reluctantly coming to the same conclusion after digging around in the API a lot, the thing is it is more like 15 extra routes not 2…

    I think I may just leave it with my original routes solution and just write a custom pagination element for the single instance where the problem occurs.

    Perhaps this is just one of those small annoyances I’ll just have to live with, sometimes workarounds are where its at.

    John

  • cakebaker

    @John: If the extra routes follow the same pattern you could generate those routes with a foreach loop. Technically, you will still have 15 extra routes, but at least it is more compact.

    It’s just an idea, I don’t know whether that’s practical in your specific situation…

  • John

    @cakebaker

    Hi – in the end I have left my routes as they were originally and I have extended the paginator helper.

    In the extended paginator helper I’ve over-ridden the link() method and am building the url directly using the data that was passed to the view in $this->passedArgs as a basis.

    Thanks for your ideas.

  • Snook.ca Easier Static Pages for CakePHP 1.2 Update | Cakephp

    […] time I took a closer look at how the dispatch was being handled. Daniel Hofstetter made mentioned how using the error handler seemed inappropriate since it’s for handling errors. Unfortunately, the Dispatcher looks for a missing action or […]

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License