Sitemap decorator

Sitemap decorator

This turned out to be an excercise in decorators: three separate decorators - in one file -working together to create a configurable sitemap at the bottom of each page:

  • SitemapDecorator: a Page decorator - adding some per page settings
  • SitemapDecorator_Config: a SiteConfig decorator - some site-wide settings
  • SitemapDecorator_Controller: a Page_Controller extension, responsible for showing the sitemap

Site-wide settings

Some settings to determine the 'root' of the sitemap. Either:

  • show the entire sites
  • how from the current page down
  • show the site from some current page down (handy to show a list of all products on each page)

Other settings:

  • the number of columns (in case of a horizontal sitemap)
  • include hidden pages
  • set the max menu-depth

Per page settings

Some simple settings to determine how, if at all, a page is displayed on the sitemap:

  • show as string (I use this to display certain category pages as 'titles')
  • exclude this page from the sitemap
  • disable the sitemap for this page

Step 1. The file

Start by creating the file SitemapDecorator. php like this:

<?php
class SitemapDecorator extends DataObjectDecorator {

}

class SitemapDecorator_Config extends DataObjectDecorator{

}

class SitemapDecorator_Controller extends Extension {

}

Step 2. The Page decorator

This class, that manages the per page settings, is fairly straightforward: it creates a couple of checkboxes in the Page's Behaviour tab:

class SitemapDecorator extends DataObjectDecorator {

	function extraStatics() {
		return array(
			'db' => array(
				'HideFromSitemap' => 'Boolean',
				'SitemapAsTitle' => 'Boolean',
				'DisableSitemap' => 'Boolean',
			)
		);
	}


	/*
	 * Add the following fields to the Page Behaviour tab
	 */
	public function updateCMSFields(FieldSet $fields) {

 		$fields->addFieldsToTab(
			"Root.Behaviour", array(
			new CheckboxField(
				'HideFromSitemap',
				_t('Sitemap.HIDEFROMSITEMAP', 'Hide this page from the sitemap')
			),
			new CheckboxField(
				'SitemapAsTitle',
				_t('Sitemap.SITEMAPASTITLE', 'Show this page without link')
			),
			new CheckboxField(
				'DisableSitemap',
				_t('Sitemap.DISABLESITEMAP', 'Disable sitemap for this page')
			)
		));
	}
}

Step 3. the SiteConfig decorator

Again, fairly straightforward: a couple of settings that will appear in their own 'Sitemap' tab in the site configuration screen. First lets create a couple of database fields and some default settings.

class SitemapDecorator_Config extends DataObjectDecorator{

	function extraStatics() {
		return array(
			'db' => array(
				'SitemapOptions' => "Enum('childtree,alltree,customtree', 'childtree')",
				'MaxLevel' => 'Int',
				'ShowHiddenPages' => 'Boolean'
			),
			'has_one' => array(
				'RootPage' => 'SiteTree'
			),
			'defaults' => array(
				'MaxLevel' => '2',
				'SitemapOptions' => 'childtree',
				'ShowHiddenPages' => false
			),
		);
	}
}

I'm using an Enum to set the sitemap's rootpage:

- alltree: show entire site,
- childtree: always use the current page as root, (default, whyt not)
- customtree: use some fixed page as root on all pages.

If 'customtree' is checked, a root page needs to be selected from a TreeDropdownField. I'll add some JavaScript later on to hide tthe the dropdown if one of the other options is selected. So now we'll add a 'Sitemap' tab to the siteconfiguration screen, only accessible to the Admin (which I like, but you don't have to do). Again in the SitemapDecorator_Config class, add te following function:

public function updateCMSFields(FieldSet $fields) {

	if (Permission::check('ADMIN')) {

		Requirements::javascript('mysite/javascript/SitemapDecorator.js');

		// set the name for the Sitemap settings tab
		$formTab = _t('Sitemap.TAB', 'SitemapSettings');

		// add a header for the the tab
		$fields->addFieldToTab(
			"Root.$formTab",
			new HeaderField('SitemapHeader',_t('Sitemap.SETTINGS', 'Sitemap Settings')
		));

		// select type of sitemap: radio group
		$fields->addFieldToTab(
			"Root.$formTab",
			new OptionsetField(
				$name = 'SitemapOptions',
				$title = _t('Sitemap.SITEMAPOPTIONS','Select the type of sitemap'),
				$source = array(
					'childtree' => _t('Sitemap.CHILDTREE', 'Show from the current page up'),
					'alltree' => _t('Sitemap.ALLTREE','Show the entire site'),
					'customtree' => _t('Sitemap.CUSTOMTREE','Show up from the page selected below:')
				),
				$value = 'alltree'
		));

		// select a custom rootpage from the sitetree
		$fields->addFieldToTab(
			"Root.$formTab",
			new TreeDropdownField('RootPageID', _t('Sitemap.ROOTPAGE','Select a custom page'),'SiteTree'
		));

		// select max menu depth
		$fields->addFieldToTab(
			"Root.$formTab",
			new TextField('MaxLevel', _t('Sitemap.MAXLEVEL','Max menu depth')
		));

		// Show hidden pages yes/no
		$fields->addFieldToTab(
			"Root.$formTab",
			new CheckboxField('ShowHiddenPages', _t('Sitemap.SHOWHIDDENPAGES','Show hidden pages')
		));

		// return the fieldset object
		return $fields;
	}
}

Step 4: a bit of JavaSript

As you can see, I'v included an requirement SitemapDecorator at the beginning of the updateCMSFields() function. This bit of JavaScript will hide/show the TreeDropdownField based on which option is selected. It is an optional feature, the JavaScript is based on how SilverStripe does something simular in the Access section.

/mysite/javascript/SitemapDecorator.js

Behaviour.register({

        '#Form_EditForm_SitemapOptions' : {
		  initialize : function() {

			var childtreeEl  = $('Form_EditForm_SitemapOptions_childtree');
			var alltreeEl    = $('Form_EditForm_SitemapOptions_alltree');
			var customtreeEl = $('Form_EditForm_SitemapOptions_customtree');

			if(childtreeEl) {
				childtreeEl.onclick = this.rootClick.bind(this);
			}
			if(alltreeEl) {
				alltreeEl.onclick = this.rootClick.bind(this);
			}
			if(customtreeEl) {
				customtreeEl.onclick = this.showHide;
			}
			this.showHide();
		},

		rootClick : function() {
			$('Form_EditForm_RootPageID').setValue(0);
			this.showHide();
		},

		showHide : function() {
			var childtreeEl  = $('Form_EditForm_SitemapOptions_childtree');
			var alltreeEl    = $('Form_EditForm_SitemapOptions_alltree');
			if((childtreeEl && childtreeEl.checked) || (alltreeEl && alltreeEl.checked)) {
				Element.hide('RootPageID');
			} else {
				Element.show('RootPageID');
			}
		}
	}
});

This can probably done in a better, more jQuery way, but for version 2.4.x at least, this works fine.

Step 5. Displaying the sitemap

Basically a sitemap is nothing more then a mult-layer menu - so rendering the sitemap could be done in a couple of nested controls in a template. Unfortunately this will only work as long as you know the 'menu depth' of the sitemap in at the moment the template is built - and that might differ for every site you create...

Recursion...
This spells recursion - and you can't really use recursion in a template. I can think of only two ways to do this. In both cases a recursive controller function is used, but then one way is to just have each recursive call render the same simple template over and over again - or just return the HTML itself. The last method isn't very nice - but then the first might create some considarable overhead. Starting with the first one:

class SitemapDecorator_Controller extends Extension {


	public $MaxLevel;
	public $SitemapOptions;
	public $ShowHiddenPages;
	public $RootPageID;
	public $SitemapAsString;

	// only set this if you are using horizontal columns
	public static $horizontal_columns = 0;


	/*
	 * Setup the conditions for the sitemap, then build it
	 */
	function Sitemap() {
		$this->MaxLevel= $this->owner->SiteConfig()->MaxLevel;
		$this->SitemapOptions = $this->owner->SiteConfig()->SitemapOptions;
		$this->ShowHiddenPages= $this->owner->SiteConfig()->ShowHiddenPages;
		$this->RootPageID = $this->owner->SiteConfig()->RootPageID;
		$this->SitemapAsString = '';

		return $this->BuildSitemapAsString();
	}


	/*
	 * return the sitemap as a HTML string (version without template)
	 */
	public function BuildSitemapAsString ($pages = null, $level = 1) {

	}
}

Now for the BuildSitemapAsString() function. What it does is:

  • get the first set of pages based on what 'root page' to use
  • recursively travel all page children, creating an unordered list for each group of sibblings
  • return the combined lists as one textstring to be added to the page template

Because I mostly use this in a multi-column sitemap at the bottom of the page, I add a class="top" to the toplevel list, and make sure a 'first' and 'last' class is added to the leftmost and rightmost columns to enable floating. Adept to your liking...

public function BuildSitemapAsString ($pages = null, $level = 1) {

	// don't show pages that have HideFromSitemap
	$extraFilter  = ' AND `HideFromSitemap` = 0 ';

	// get the first set of pages
	if (empty($pages)) {

		// determine what root page to use for the sitemap
		switch ($this->SitemapOptions) {
			case 'customtree':
				$parentID = $this->RootPageID;
				break;
			case 'childtree':
				$parentID = $this->owner->ID;
				break;
			default:
				$parentID = 0;
		}

		// get the first set of pages
		$pages = DataObject::get('SiteTree', "ParentID = {$parentID}" . $extraFilter);
	}

	if (!empty($pages)) {

		$this->SitemapAsString .= "<ul>";
		$counter = 0;
		foreach($pages as $page) {

			$class = ($level == 1)? 'class="top"' : '';

			$counter++;
			// on a horizontal sitemap you might need to set the number of columns
			// creating a 'first' and 'last' class
			if ($level == 1){

				if (self::$horizontal_columns){
					if ($counter == self::$horizontal_columns) {
						$class = str_replace('top', 'top last', $class);
						$counter = 0;
					}
					elseif ($counter == 1) {
						$class = str_replace('top', 'top first', $class);
					}
				}
			}

			// only those pages the user may view
			if ($page->can('view')) {

				$this->SitemapAsString .= 
					'<li ' . $class . '> <a ' . $class . ' href="' . $page->Link() . 
					'" title="' . $page->MenuTitle .'">' . $page->MenuTitle . "</a>";

				// Check for children...
				$children = DataObject::get('SiteTree', '`ParentID` = ' . $page->ID . $extraFilter);

				if (!empty($children)) {

					// ...if this is not the max level...
					if ($level < $this->MaxLevel) {
						// call this method recursively
						$this->BuildSitemapAsString($children, $level+1);
					}
				}
			}
			$this->SitemapAsString .= "</li>";
		}
		$this->SitemapAsString .= "</ul>";
	}
	return $this->SitemapAsString;
}

Step 6. Switching it on

In your _config.php file:

// add a sitemap to (every) page.
DataObject::add_extension('Page', 'SitemapDecorator');
Object::add_extension('Page_Controller', 'SitemapDecorator_Controller');
DataObject::add_extension('SiteConfig', 'SitemapDecorator_Config');

// optional: determine the number of columns for the sitemap
SitemapDecorator_Controller::$horizontal_columns = 5;

Step 7. The template

From your template: the only thing you need is something like this:

<div id="sitemap">
	$Sitemap
</div>

That's it. There are many things you could add, but that would only give this page an extra yard. But it's open for improvements...

Comments

Het versturen van reacties is uitgeschakeld.

RSS feed voor reacties op deze pagina