Concrete5

Creating Year and Month Archives with Page Type and Template

It's been way too long since I published the article, "Auto-generate Page Locations Based on Date in Concrete5.7". In that article, I mention that posts made to this site are automatically grouped beneath year and month pages, based on their date. This enables a blog-like URL structure:

http://andrewembler.com/2015/05/auto-generate-page-locations-based-date-concrete57/

The post actually begins in the drafts folder, and once it's published, it's automatically moved into the proper location in the sitemap, and the month and year pages are automatically created if they don't already exist. The original article can tell you more. Then, there's this:

We add pages of the type "Archives Year" and "Archives Month" – which we'll talk about later. These pages use custom controller to grab lists of pages matching the specific month or year, and inject those into an archive listing page.

Well, it's finally time to give you the details. The code in the previous article will create pages of a given type, with a given template, but unless you set those pages up with custom code, they'll simply be blank when you navigate to them. By contrast, check out what happens on my site when you to the various archive URLs (don't worry, I'll wait.):

http://andrewembler.com/2015/05/

http://andrewembler.com/2015/

The relevant posts from that time period are displayed in the page, with pagination. Here's how it works.

Step 1: Create Two Page Types

I've created two page types, one for "Archives Month" and one for "Archives Year"

Step 2: Create a Page Template

I create a single page template, for my Archives page.

Ensure the proper Page Types and Template are used

In the custom handler code added in the other article, double-check that the proper page type and template are used. Here is the relevant code:

$posts = Page::getByPath('/posts');
$year = Page::getByPath('/' . $y);
if (!is_object($year) || $year->isError()) {
    $pt = Type::getByHandle('archives_year');
    $year = $posts->add($pt, array(
        'name' => $y
    ), Template::getByHandle('archives'));
    $year->setCanonicalPagePath('/' . $y);
}
$month = Page::getByPath('/' . $y . '/' . $m);
if (!is_object($month) || $month->isError()) {
    $pt = Type::getByHandle('archives_month');
    $month = $year->add($pt, array(
        'name' => $m
    ), Template::getByHandle('archives'));
}

Notice we are using the page types with the handles "archives_month" and "archives_year", and the page template with the handle "archives." If you've given your types and templates different handles, make sure to change them in this code.

To recap: any time that a post is placed in these automatically created pages, the year page will be a page of type "Archives Year", with the template "Archives," and the month page will be a page of the type "Archives Month", with the template "Archives."

Set Up the Year Page Type Controller

We use two separate page types because we're going to want to have two separate page controllers run when pages of these type are visited. First, create a page type controller for the "Archives Year" page type. Since the page type handle is "archives_year", we'll create the page type at "application/controllers/page_types/archives_year.php." Within it goes this code:

<?php
namespace Application\Controller\PageType;
use Concrete\Core\Page\PageList;
use Core;

class ArchivesYear extends \Concrete\Core\Page\Controller\PageController
{
    public function view()
    {
        $year = intval($this->getPageObject()->getCollectionHandle());
        $this->set('name', 'Archives');
        $this->set('description', $year);

        $pl = new PageList();
        $pl->sortByPublicDateDescending();
        $pl->filterByPageTypeHandle('post');
        $pl->filterByPublicDate($year . '-01-01 00:00:00', '>=');
        $pl->filterByPublicDate($year . '-12-31 23:59:59', '<=');
        $pagination = $pl->getPagination();
        $this->set('list', $pl);
        $this->set('pages', $pagination->getCurrentPageResults());
        $this->set('pagination', $pagination);

        $seo = Core::make('helper/seo');
        $seo->setCustomTitle('Archives')->addTitleSegment($year);
    }
}

(Note, in my actual site, I've shipped all of this custom code inside a custom package, which means my namespaces are different than in this example – but for simplicity's sake this example assumes you're making these changes within your site's custom application directory.)

This is pretty easy to understand. A page type controller automatically runs its view() method any time a page of the type is visited. First, we determine what year we're supposed to be viewing, by getting the handle of the current page. The handle is usually the final segment of the URL, and that's what is in this case. (We set this automatically in the handler above when we created the year page.) Next, we set some custom data into the page template using the controller's set() method.

Finally, we instantiate a custom page list, and use the pages year to only display those pages that fall between that year. We send a $pagination variable, and an array of $pages into our page template – which we'll talk about in a moment.

Set Up the Month Page Type Controller

We do something very similar for the month. Create a controller at "application/controllers/page_types/archives_month.php", and place this code in it:

<?php


namespace Application\Controller\PageType;
use Core;

class ArchivesMonth extends \Concrete\Package\Ae2014\Page\PageController
{
    public function view()
    {
        $date = Core::make('date');
        $path = $this->getPageObject()->getCollectionPath();
        $path = explode('/', trim($path, '/'));
        $start = $date->toDateTime($path[0] . '-' . $path[1] . '-01');
        $this->set('name', 'Archives');
        $this->set('description', $start->format('F Y'));

        $pl = new PageList();
        $pl->sortByPublicDateDescending();
        $pl->filterByPageTypeHandle('post');
        $pl->filterByPublicDate($start->format('Y-m-d 00:00:00'), '>=');
        $pl->filterByPublicDate($start->format('Y-m-t 23:59:59'), '<=');
        $pagination = $pl->getPagination();
        $this->set('list', $pl);
        $this->set('pages', $pagination->getCurrentPageResults());
        $this->set('pagination', $pagination);

        $seo = Core::make('helper/seo');
        $seo->setCustomTitle('Archives')->addTitleSegment($start->format('F Y'));
    }
}

This code is very similar – it simply retrieves the year and the month from the page path, and filters by posts that fall within that time instead. (Note: with a bit of work you could probably make both of these page type controllers extend a unified controller, since so much of this code is duplicated between the two controllers. But that is a project for another day.)

Note how this page type controller also sets a $pagination and $pages variable into the page template. That's because our single page template uses this data from both page type controllers.

Implement Page Template

Finally, in our theme, we create a single archives page template. (application/themes/your_theme\archives.php.) This page template is responsible for looping through all the pages provided by either page type controller, as well as displaying pagination. Here's how mine looks:

<?php $view->inc('elements/header.php'); ?>

    <div id="content" class="">
        <div class="inner">
            <div class="block_general_title_1">
                <h1><?=$name?></h1>
                <h2><?=$description?></h2>
            </div>

            <?php if (count($pages)) { ?>
            <div class="block_posts type_5 type_sort general_not_loaded">
                <div class="posts">
                <?php foreach($pages as $c) { // display the page data in some way here...

                <?php } ?>
                </div>
                <?php if ($pagination->haveToPaginate()) { ?>
                    <div class="separator" style="height:43px;"></div>
                    <?=$pagination->renderDefaultView();?>
                <?php } ?>

            </div>


            <?php } else { ?>
                <p><?=t('No posts found.')?></p>
            <?php } ?>
        </div>
    </div>

<?php $view->inc('elements/footer.php');

Going Further

This archives template could be used for more than just these date archives pages. Any page type controller that sets the required variables into the template can be used. Hopefully this gets your mind working about how templates and page type controllers work together, and how you can use that relationship to create modular, flexible code.

Loading Conversation