BLOG

If you need your composer pages to exist in a specific site structure (such as years/months/categories), you can automate this. Here's a tutorial explaining how.

composer-auto-site-tree.jpg

How to get Composer to automatically create sitemap structure in Concrete5 5.6

21st March 2016

One of the things I think needs improvement in Concrete5 is the way it suggests you structure your site; it's all well and good being able to simply publish pages, but there'll come a point where you simply have too many pages under a single directory.

So supposing you were creating a "Blog" or "News" section, standard composer rules would suggest that whatever you create is simply placed underneath your "blog/news" page. However, for most organisations you'll end up with hundreds of pages in a single pile. So in many instances I'll structure it like:

Blog > Year > Month > Post

That way you'll end up with:

Blog > 2016 > January > Happy New Year!

That way you keep the sitemap clean. But it can be annoying to manually create the site structure each time, so here's how you'd automate it.

Remember this is all for Concrete5 5.6 (the process for 5.7 is likely to be a bit different).


Step 1: Create the event hook

In config, create (or edit) a "site_events.php" file. The following code can then be added.

    // arguments are (event, class, function, file)
    Events::extend('on_composer_publish', 'BlogPublishingModel', 'setupArchitecture', 'models/blog-publishing.php');

This event hook is triggered when the composer publish event is triggered. It invokes the function in the identified class (on the identified path). Let's move on to...


Step 2: Create the blog publishing model

The event hook calls the blog publishing model (or whatever class you want). I've created:

models > blog_publishing.php

This is the class that I'll use to handle the event. What this class needs to do is:

  1. Get the year needed for the page
  2. Get the month needed for the page
  3. See if the page exists for that year (if not, create it)
  4. See if the page exists for that month (if not, create it)
  5. Save the page underneath the month page

So let's create the outline class:

class BlogPublishingModel
{
    public static $page;
    public static $date;
    public static $month;
    public static $year;
    public static $monthID;
    public static $yearID;
    
    // the path to where you want the directory structure to be added under
    public static $blog_path = '/blog/'; 
    
    /**
     * Function called by the Concrete5 event hook for both composer save and composer publish events
     * @param $page
     */
    public function setupArchitecture($page)
    {
        // set the page property
        self::$page = $page;
        
        // determine the month and year by the page's public date
        self::setDate();

        // determine whether the month/year pages exist (and create them if not)
        self::setupPaths();

        // get the page object for the new parent (the month)
        $newPage = Page::getById( self::$monthID );

        // move the page to under the new parent (the month) rather than at root (which is what composer proposes)
        self::$page->move( $newPage );

        // return the page so that the rest of the functionality can continue to work as expected
        return self::$page;
    }
    
}

So that's the outline for the setupArchitecture function. However, this calls various pieces of functionality to get the month/year for the requested page, check whether those paths exist and create them if they don't.

So let's look at those pieces of functionality.

    /**
     * Sets the date from the page object into the class attribute
     */
    protected function setDate()
    {
        // set the date attribute based on the page public date
        self::$date = self::$page->getCollectionDatePublic();
    }

    /**
     * Setup the various page paths and set the class attributes for each
     */
    protected function setupPaths()
    {
        self::yearExists();
        self::monthExists();
    }

    /**
     * This checks whether the month folder exists on the desired path. If not, it creates it.
     * The monthID attribute is then set on this class with whatever the page ID is.
     */
    protected function monthExists()
    {
        // get the month by the page date
        $date = self::$date;
        $month = date('F',strtotime($date));
        self::$month = $month;

        // setup the path to check whether the page exists or not
        // this will look for it like /blog/2016/january
        $path = self::$blog_path . self::$year . '/' . strtolower($month);

        $month_page = Page::getByPath($path);

        // does the page exist?
        if ( $month_page->cID ) {

            // page found, set the month ID
            self::$monthID = $month_page->cID;

        } else {

            // page doesn't exist, we need to create it
            $month_page = self::addMonthPage();
            self::$monthID = $month_page->cID;
        }

    }

    /**
     * This checks whether the year folder exists on the desired path. If not, it creates it.
     * The yearID attribute is then set on this class with whatever the page ID is.
     */
    protected function yearExists()
    {
        // get the year from the page date
        $date = self::$date;
        $year = date('Y',strtotime($date));
        self::$year = $year;

        // setup the path to check whether the page exists or not
        // this looks for the page at /blog/2016
        $path = self::$blog_path . self::$year;

        $year_page = Page::getByPath($path);

        // is there a page there?
        if ( $year_page->cID ) {

            // page found, set the month ID
            self::$monthID = $year_page->cID;

        } else {

            // page doesn't exist, we need to create it
            $year_page = self::addYearPage();
            self::$monthID = $year_page->cID;
        }
    }

    /**
     * Create the page object for the month under the given year
     * @return page
     */
    protected function addMonthPage()
    {
        // so this creates a page at /blog/2016/
        $parentPage = Page::getByPath(self::$blog_path . self::$year);
        $ct = CollectionType::getByHandle("blog");

        // set the page attributes for name and description
        $data = array();
        $data['cName'] = self::$month;
        $data['cDescription'] = self::$month;

        // create the page
        $newPage = $parentPage->add($ct, $data);
        return $newPage;
    }

    /**
     * Create the page object for the given year
     * @return page
     */
    protected function addYearPage()
    {
        // so this will add a page at /blog/
        $parentPage = Page::getByPath( self::$blog_path );
        $ct = CollectionType::getByHandle("blog");

        // set the page attributes for name and description
        $data = array();
        $data['cName'] = self::$year;
        $data['cDescription'] = self::$year;

        // add the page
        $newPage = $parentPage->add($ct, $data);
        return $newPage;
    }

Add all of these pieces together and you've got a script that will create the month and year pages for you in a directory structure.

You can obviously do more in this process; you can set page attributes on the month/year pages, set the page type to use - I've really only demonstrated a barebones implementation here. You could use the same approach to create category pages or however you want to structure your content.

I've just used the /blog/year/month/ structure because it's one I use commonly for news or blog posts.

Pro-tip: you can make this a lot more slick by setting certain attributes on the month/year pages. I've previously implemented a "landing_page" attribute which (if set on the page) will automatically display a page-list hard-coded into the page type to list the news/blog posts beneath it. That would mean that hitting the "2016" page would show you the months that exist underneath it. Hitting the "2016/January" page would show all the blog posts for within January. The fact is that most users will only ever follow the user journey of:

blog > post

Very few will traverse the entire site tree manually, but it's useful from an architectural standpoint to prevent C5 needing to deal with a single "blog" page beneath which every single blog post ever written is housed. Not overly scalable. This method is.

I hope you found this useful.

THINK THERE'S SOMETHING WE CAN TALK ABOUT?

Whether you want to discuss a piece of business, get advice on how to approach something in Concrete5, want my top Project Zomboid tips or just simply want to say hello, then please do. I'm not as anti-social as my status as a developer would suggest.

GET IN TOUCH

CONTACT

Image
LOCATION
Milton Keynes (near London)
Image
PHONE
Disclosed for business only
Image
E-MAIL
steven.york [at] seopher.com
Image
AVAILABILITY
Contact me to find out