A sitemap for a website is basically a single place that links to all the other pages (and posts) on that site, so human visitors can scan through to find what it is they may be after. The XHTML sitemap to which I refer is not the sitemap.xml that Google and other search engines support, which also includes values on how often those pages should be visited by their spiders.
You could add a normal page, and manually add a link every time you create a new post or page, but of course, there are more efficient ways of doing it. I've already offered examples on how this may be done programatically within the Thesis theme but even this could be improved upon, and made more generic to apply to any theme, by using a WordPress shortcode instead.
The Class File
The class file below is suitable only for PHP5 installations, and WILL cause errors if you have PHP4. Luckily, you can use the version for PHP4 instead ;)
First, add the following to a new file called GT_Sitemap.php, and upload it to your theme. For Thesis theme users I recommend adding a classes folder inside the custom directory and saving the file there.
<?php
/**
* Allows XHTML sitemap to be added
*
* @package GT_Sitemap
* @author Gary Jones
* @version 2010-05-19
* @since 2010-03-20
*/
class GT_Sitemap {
/**
* @var string
*/
protected $_pagesText;
/**
* @var string
*/
protected $_postsText;
/**
* @var string
*/
protected $_archivesText;
/**
* @var int
*/
protected $_headingLevel;
/**
* @var array
*/
protected $_order = array();
/**
* @var bool
*/
protected $_showPageDate;
/**
* @var string
*/
protected $_showPostDate;
/**
* @var bool
*/
protected $_showPostCount;
/**
* @var string
*/
protected $_archivesType;
/**
* @var string
*/
protected $_pagesDateFormat;
/**
* @var string
*/
protected $_postsDateFormat;
/**
* @var string
*/
protected $_customPagesQuery = '';
/**
* PHP4 compatible constructor
*/
public function GT_Sitemap() {
if(version_compare(PHP_VERSION,"5.0.0","__construct")) {
register_shutdown_function(array($this,"__destruct"));
}
}
/**
* PHP5 constructor, setting defaults
*/
public function __construct() {
$this->_pagesText = 'Pages';
$this->_postsText = 'Posts';
$this->_archivesText = 'Monthly Archives';
$this->_headingLevel = 3;
$this->_order = array('pages', 'posts', 'archives');
$this->_showPageDate = true;
$this->_showPostDate = 'published';
$this->_showPostCount = true;
$this->_archivesType = 'monthly';
$this->_pagesDateFormat = get_option('date_format');
$this->_postsDateFormat = get_option('date_format');
$this->_customPagesQuery = '';
}
/**
* @param string $id
*/
public function setPagesText($id) {
$this->_pagesText = $id;
return $this;
}
/**
* @param string $id
*/
public function setPostsText($id) {
$this->_postsText = $id;
return $this;
}
/**
* @param string $id
*/
public function setArchivesText($id) {
$this->_archivesText = $id;
return $this;
}
/**
* @param array $id
*/
public function setOrder($arg1, $arg2 = null, $arg3 = null) {
$this->_order = func_get_args();
return $this;
}
/**
* @param int $id
*/
public function setHeadingLevel($id) {
$this->_headingLevel = $id;
return $this;
}
/**
* @param string $id
*/
public function setPageDateFormat($id) {
if ( 0 === func_num_args() ) {
$this->_showPageDate = '';
} else {
$this->_pagesDateFormat = $id;
}
return $this;
}
/**
* @param string $id
*/
public function setPostDateFormat($id) {
if ( 0 === func_num_args() ) {
$this->_showPostDate = '';
} else {
$this->_postsDateFormat = $id;
}
return $this;
}
/**
* @param string $id
*/
public function setDateFormat($id) {
$this->setPageDateFormat($id);
$this->setPostDateFormat($id);
return $this;
}
/**
*
*/
public function hidePostCount() {
$this->_showPostCount = false;
return $this;
}
/**
* @param string $id
*/
public function setArchivesType($id) {
$archiveTypes = array('yearly', 'monthly', 'daily', 'weekly', 'postbypost', 'alpha');
if ( in_array($id, $archiveTypes) ) {
$this->_archivesType = $id;
$this->setArchivesText(substr_replace($id, strtoupper(substr($id, 0, 1)), 0, 1) . ' Archives');
}
return $this;
}
/**
* @param string $id
*/
function setCustomPagesQuery($id) {
$this->_customPagesQuery = $id;
return $this;
}
/**
* @param string $shortcode The shortcode keyword that will be used to output the sitemap
*/
public function shortcode($shortcode) {
add_shortcode( $shortcode, array(&$this, 'build') );
}
/**
* Does the main work of creating the output
*/
public function build() {
foreach ($this->_order as $section) {
if ( 'pages' === $section ) {
$output .= '_headingLevel . '>' . $this->_pagesText . '_headingLevel . '>' . "\n"
. '<ul>' . "\n" . wp_list_pages('echo=0&show_date=' . $this->_showPageDate . '&date_format=' . $this->_pagesDateFormat . '&title_li=&' . $this->_customPagesQuery) . '</ul>' . "\n";
}
if ( 'posts' === $section ) {
$output .= '_headingLevel . '>' . $this->_postsText . '_headingLevel . '>'."\n"
. '<ul>' . "\n" . $this->_posts_by_category() . '</ul>' . "\n";
}
if ( 'archives' === $section ) {
$output .= '_headingLevel . '>' . $this->_archivesText . '_headingLevel . '>' . "\n"
. '<ul>' . "\n" . wp_get_archives('type=' . $this->_archivesType . '&echo=0&show_post_count=' . $this->_showPostCount). '</ul>' . "\n";
}
}
return $output;
}
protected function _posts_by_category() {
global $wpdb, $post;
$tp = $wpdb->prefix;
$sort_code = 'ORDER BY name ASC, post_date DESC';
$the_output = NULL;
$last_posts = (array)$wpdb->get_results("SELECT {$tp}terms.name, {$tp}terms.term_id, {$tp}term_taxonomy.term_taxonomy_id FROM {$tp}terms, {$tp}term_taxonomy WHERE {$tp}terms.term_id = {$tp}term_taxonomy.term_id AND {$tp}term_taxonomy.taxonomy = 'category'");
if (empty($last_posts)) {
return NULL;
}
$the_output .= '';
$used_cats = array();
$i = 0;
foreach ($last_posts as $posts) {
if (in_array($posts->name, $used_cats)) {
unset($last_posts[$i]);
} else {
$used_cats[] = $posts->name;
}
$i++;
}
$last_posts = array_values($last_posts);
foreach ($last_posts as $posts) {
$the_output .= '<li><a>term_id) . '"><strong>' . apply_filters('list_cats', $posts->name, $posts) . '</strong></a><ul>';
$arcresults = $wpdb->get_results("SELECT * FROM $wpdb->posts WHERE post_type = 'post' AND post_status = 'publish' AND ID IN (SELECT object_id FROM {$tp}term_relationships, {$tp}terms WHERE {$tp}term_relationships.term_taxonomy_id =" . $posts->term_taxonomy_id . ") ORDER BY post_date DESC");
foreach ( $arcresults as $arcresult ) {
$the_output .= '<li><a>ID) . '">' . apply_filters('the_title', $arcresult->post_title) . '</a> ';
if ($this->_showPostDate) {
$the_output .= date($this->_postsDateFormat,strtotime($arcresult->post_date));
}
$the_output .= '</li>';
}
$the_output .= '</ul></li>';
}
return $the_output;
}
}
The really great thing about using classes is that you don't need to understand any of the above (though in terms of PHP and WordPress development, it's not that tricky), as within it are some easy-to-understand functions that you can use to customise everything.
Basic Usage
Within your functions.php (or custom_functions.php file if you're using Thesis theme) the basic set up requires just three lines of code!
require_once 'classes/GT_Sitemap.php';
$sitemap = new GT_Sitemap;
$sitemap->shortcode('sitemap');
The first line is the path to class file - I'm using Thesis so mine is stored within the classes folder. You only need this line once, however many sitemap sections you want to be displayed.
The second line creates a sitemap object. Nothing really to change here - so long as the variable on the left ($sitemap in this example) is not already being used elsewhere, and that it's the same as the start of the third line, then it's all good.
The third line is where some of the fun starts. The basic usage is simply to call the shortcode method, along with the shortcode word you want to use (here, it's sitemap).
All that's left, is to add the shortcode (here, it would be [sitemap]) to a Post or Page to display the output.
Default Output
The sitemap for this site acts as a demo for the basic usage of this code.
Customisations
What if you want to customise the output in some way? Would you have to edit that big class file? Absolutely not! You just make your settings before calling the shortcode method. All of the demo examples are effectively showing a replacement for the third line of the code above.
setPagesText()
Change the default Pages subtitle before the Pages section.
$sitemap->setPagesText('All Pages:')->shortcode('sitemap');
setPostsText()
Change the default Posts subtitle before the Posts section.
$sitemap->setPagesText('All Posts:')->shortcode('sitemap');
setArchivesText()
Change the default Monthly Archives subtitle before the Archives section. As the setArchivesType() updates this value as well to keep the title in sync with the archive type, setArchivesType should be called afterwards if you don't like the default format.
$sitemap->setPagesText('Archives:')->shortcode('sitemap');
setArchivesType()
Change the default monthly archives to one of the following: yearly, monthly, weekly, daily.
$sitemap->setArchivesType('weekly')->shortcode('sitemap');
setOrder()
Default output order is 'pages', 'posts', 'archives'. You can change the order.
$sitemap->setOrder('posts', 'archives', 'pages')->shortcode('sitemap');
This is also useful if you perhaps want to add your own content between sections of the sitemap.
$sitemapposts = new GT_Sitemap;
$sitemapposts->setOrder('posts')->shortcode('sitemapposts'); // Use [sitemapposts] shortcode
$sitemappages = new GT_Sitemap;
$sitemappages->setOrder('pages')->shortcode('sitemappages'); // Use [sitemappages] shortcode
In your Post or Page, you might have something like:
Here is a list of all my posts: [sitemapposts] The pages however are not updated that often, but they contain static content of some sort. [sitemappages]
setHeadingLevel()
Default heading level for the section subtitles is <h3>…</h3> You can change this to another <hx>.
$sitemap->setHeadingLevel(2)->shortcode('sitemap');
setPageDateFormat()
Default date format is whatever you've got set in the WordPress General Settings. You can change this for the date output next each Page. The abbreviations follow the the standard PHP date format codes. If the value is left empty, the date will be hidden.
$sitemap->setPageDateFormat('Y-m-d')->shortcode('sitemap');
setPostsDateFormat()
Default date format is whatever you've got set in the WordPress General Settings. You can change this for the date output next each Post. The abbreviations follow the the standard PHP date format codes. If the value is left empty, the date will be hidden.
$sitemap->setPostDateFormat('d M Y')->shortcode('sitemap');
setDateFormat()
This is a shortcut for setting the setPageDateFormat() and setPostDateFormat values at the same time.
$sitemap->setDateFormat()->shortcode('sitemap'); // Remove all dates
setCustomPagesQuery()
Default is a zero-length string. Add in a wp_list_pages() query variable to show customise what's returned for the list of Pages
$sitemap->setCustomPagesQuery('exclude=182')->shortcode('sitemap'); // Exclude Page 182
hidePostCount()
Default is to show the number of posts in each of the archive groups. This number can be removed entirely.
$sitemap->hidePostCount()->shortcode('sitemap');
Putting it Altogether
All of these customisations could be combined in any order, so long as the shortcode section is last in the chain:
$sitemap->setOrder('archives', 'posts', 'pages')
->setPagesText('My Pages')
->setArchivesType('weekly')
->setArchivesText('Posts Grouped By Week')
->shortcode('sitemap');
In this example, each setting is on it's own line, as some may prefer that style of coding; it could equally have gone all on one continuous line like the previous example.
Conclusion
Using classes to add features to your WordPress site gives many advantages over procedural code that's normally used within WordPress Themes. Once a class is written (something you can pay an expert to do), then the code you have to write is minimal yet very powerful. There's far less code to potentially interact badly with any existing code in your file, and copying it to another WordPress site is as easy as copying the class file, and adding in those three lines of code. Easy!
Updated 2010-04-26: Added the setCustomPagesQuery() method to allow full control over what Pages are shown. Also added a PHP4 version.
Updated 2010-05-19: Fixed small typo that meant Pages heading was appearing in the wrong place.
This is pretty cool Gary. Could it also be automated to not create one sitemap, but rather top tier category sitemaps: one for each top tier to list its entire category silo?
Thanks Craig.
Like any classes, this could be adapted for special cases and variations on a theme, without breaking existing implementations, yes. I imagine a
setScope()method could be added, to limit the posts / time-based archives to a certain category parent.Hi Gary,
This gives me a parse error of : Fatal error: Call to undefined method GT_Sitemap::hidePostDate() in C:\xampp\htdocs\testbase\wp-content\themes\thesis_17\custom\custom_functions.php on line 22.
Which is this in my file: ->hidePostDate()
Is it that double colon that leads ::hidePostDate() ? Without the customizations, it works just fine.
I’ve updated the final example (“Putting it all together”) as it contained a method that I initially had in the class, but later removed in favour of incorporating the functionality into an existing method. Just remove the ->hidePostDate() and it should all work without error.
Gary
I setup the XHTML sitemap v2 which is based on the above code. It never occured to me that I could use a human searchable friendly version for bots too as you mention. When I submitted that to webmaster tools as the page
domain/sitemap/ but got an “error cross” once Google downloaded the file. Am I incorrect in believing that this format can be used in the place of my wp-xml sitemap generator which spits out a sitemap at domain.com/sitemap.xml…or?
Hi Craig,
Obviously, as normal links, if Google finds this page in it’s normal spidering, then it will have access to all other content on your site and has the good opportunity to index them.
However, this is not the sitemap.xml format that Google and all other major search engines refer to. In sitemap.xml, values are added which are just for search engines (like, how often the SE should return to check for changes), and isn’t really for humans.
The sitemap alluded to above IS for humans though – giving human visitors the opportunity to see all of the content on your site, without having to do random searches etc.
Forgot to update before you replied…it works like a charm with that line removed.
Gary,
Thanks for clearing up my doubts. That makes sense. Looking for a way to have an automated version of both…..your script I noticed updates to only show all pages/posts set to be indexed. (when I set a page to no-index, it disappeared magically). Qudos!
So ideally, a site needs both….one signed out for bots to find…and one (like yours) that can easily be found by site users. But the thing that made me look to your script is that xml-sitmap for WordPress will not automatically include pages IF the blog is set to a specific page. On another site, there is not blog and it does list all posts and pages. Maybe I’m setting it up incorrectly, but I don’t think so.
It also occurs to me as I write this, that there is a dearth of tutorials explaining easily the use of the custom template (for non programmers)….you’ve kind of done that here tangentially. (hint)
Craig,
My code is automated – once it’s set up, if you create a new page, then it will automatically appear on the Sitemap page.
A plugin, like the popular Google XML Sitemap Plugin for WordPress can also be automated such that it updates the sitemap.xml and sitemap.xml.gz files automatically after each post. This plugin also allows you to give a value hint to Google about how often certain areas of the site are re-indexed (daily, weekly, monthly etc).
Between my code above, and the plugin, both types of sitemaps for both types of visitors are automatically updated as content is added, changed or deleted.
In terms of custom templates, as you’ve said, there’s tutorials elsewhere, so I’m highly unlikely to add anything here – this site is partly a place for me to drop my custom written code or snippets that others have written that I think are useful. If it’s my code, then I may well add some tutorial notes to accompany it.
Thank you very much indeed, Gary. Quite nice.
Hey Gary,
I am getting an error on line 77…any idea why?
Parse error: syntax error, unexpected T_STRING in /home2/username/public_html/mysite/wp-content/themes/thesis_17/custom/classes/GT_Sitemap.php on line 77
Hi Sean,
There was an error in the code on line 76 that probably only would have been parsed if you’re running PHP4. Give that a try now.
Now I get the same on 215..
Parse error: syntax error, unexpected T_STRING in /home2/user/public_html/my domain/wp-content/themes/thesis_17/custom/classes/GT_Sitemap.php on line 215