TOP
Home > Blog > Add CMS Page Links to Magento's Top Nav Menu

Add CMS Page Links to Magento's Top Nav Menu

Posted by Derek on October 15, 2014

One of the missing features of Magento is lack of control over the main navigation menu. By default the top menu is used exclusively for product categories. I was recently working on a project where I wanted to have all the products under one sub-menu and then link to several CMS pages next to it. For stores that may have a variable number of top level categories this prevents the client from stuffing too many links in the top menu.

Magento's solution for linking to CMS pages is to create empty categories and then set up URL redirects for them. This just feels wrong to me. I don't want links to 301 redirects in my main navigation.

There are some (not free) extensions that can fix this or some people suggest hardcoding the links into the template, which doesn't work so well with responsive themes. The correct css clases may not get assigned. I then noticed the Fishpig Wordpress Integration extension has the ability to add a Wordpress menu to the Magento top menu. So I looked through the extension's code, made some modifications and landed on this:

Step One - Add some Custom Fields to CMS Pages

Before we put CMS links in the menu we need to know which ones and be able to edit the settings. I used two custom fields - a yes/no select to include the page in the menu and text field to hold the order number to sort them. The CMS entity does not follow the usual EAV pattern so it requires updating the cms_page table directly rather than adding a new attribute like you would with catalog or order. Create a new module with an observer model and a setup script. In this case I'm using the namespace "Graydog" and then "CmsLinks" for the module name. The directory structure with necessary files is:

app/code/local/Graydog/CmsLinks/etc/config.xml
app/code/local/Graydog/CmsLinks/Model/Observer.php
app/code/local/Graydog/CmsLinks/sql/graydog_cmslink_set/mysql4-instal-0.1.0.php
app/etc/modules/GrayDog_CmsLinks.xml

 

Configure the module with a setup resource and observer with two methods; one for initializing the CMS page edit form with our new fields and another for saving the custom fields. 

app/code/local/Graydog/CmsLinks/etc/config.xml
<?xml version="1.0"?>
<config>
  <modules>
    <Graydog_CmsLinks>
      <version>0.1.0</version>
    </Graydog_CmsLinks>
  </modules>
  <global>
    <models>
      <graydog_cmslinks>
        <class>Graydog_CmsLinks_Model</class>
      </graydog_cmslinks>
    </models>
    <resources>
      <graydog_cmslinks_setup>
        <setup>
          <module>Graydog_CmsLinks</module>
        </setup>
        <connection>
          <use>core_setup</use>
        </connection>
      </graydog_cmslinks_setup>
      <graydog_cmslinks_write>
        <connection>
          <use>core_write</use>
        </connection>
      </graydog_cmslinks_write>
      <graydog_cmslinks_read>
        <connection>
          <use>core_read</use>
        </connection>
      </graydog_cmslinks_read>
    </resources> 
  </global>
  <adminhtml>        
    <events>
      <cms_page_prepare_save>
        <observers>
          <graydog_cmslinks_save_page>
            <type>singleton</type>
            <class>graydog_cmslinks/observer</class>
            <method>savePage</method>
          </graydog_cmslinks_save_page>
        </observers>
      </cms_page_prepare_save>
      <adminhtml_cms_page_edit_tab_main_prepare_form>
        <observers>
          <graydog_cmslinks_prepare_form>
            <type>singleton</type>
            <class>graydog_cmslinks/observer</class>
            <method>prepareForm</method>
          </graydog_cmslinks_prepare_form>
        </observers>
      </adminhtml_cms_page_edit_tab_main_prepare_form>
    </events>
  </adminhtml>
</config>

Now create the setup script. This will add two new columns called include_in_nav and include_in_nav_order to the cms_page table.

app/code/local/Graydog/CmsLinks/sql/graydog_cmslink_setup/mysql4-install-0.1.0.php
<?php
$installer = $this;
$installer->startSetup();
 
$installer->run('ALTER TABLE `cms_page` ADD `include_in_nav` SMALLINT(6) NOT NULL DEFAULT 0');
$installer->run('ALTER TABLE `cms_page` ADD `include_in_nav_order` SMALLINT(6) NULL');
 
$installer->endSetup();

And finally, create the observer. 

app/code/local/Graydog/CmsLinks/Model/Observer.php
<?php
class Graydog_CmsLinks_Model_Observer extends Varien_Object
{

  public function prepareForm(Varien_Event_Observer $observer)
  {
    $fieldset = $observer->getForm()->getElement('base_fieldset');
    $fieldset->addField('include_in_nav', 'select',
      array(
        'name'      => 'include_in_nav',
        'required'  => true,
        'label'     => Mage::helper('cms')->__('Include in Top Nav Menu'),
        'values'    => array('' => 'Select', '0' => 'No', '1' => 'Yes'),
        'disabled'  => false,
        'value'     => Mage::registry('cms_page')->getIncludeInNav()
    ));
    $fieldset->addField('include_in_nav_order', 'text',
      array(
        'name' => 'include_in_nav_order',
        'required' => false,
        'label' => Mage::helper('cms')->__('Order in Top Menu'),
        'disabled' => false,
        'value' => Mage::registry('cms_page')->getIncludeInNavOrder()
    ));
  } 

  public function savePage(Varien_Event_Observer $observer)
  {
    $model = $observer->getEvent()->getPage();
    $request = $observer->getEvent()->getRequest();
    $data = $request->getPost();
    
    if ($data['include_in_nav_order']) {
      $model->setIncludeInNavOrder($data['include_in_nav_order'])->save();
    }

  }

}

Note that there is no handling of the include_in_nav field in the savePage method. For some reason this field would save without it. It took me while to figure why the other field was not saving and I added this method. I haven't taken the time to figure out what would make them different, but if you're having trouble saving include_in_nav you may have to update this method. Now hit any page on the front-end of the site to run the setup script. Check that the new columns have been created. If the setup failed check the logs and if need delete the entry for your module from core_resources to run it again. If succesfull your CMS page edit form should now look like this: 

Step Two - Create an Observer Function to Add Links to Menu

Create a new function in the observer that will run on the page_block_html_topmenu_gethtml_before event. Update the config.xml at app/code/local/Graydog/CmsLink/etc/config.xml by adding the following tag:

<?xml version="1.0"?>
<config>
........
  <frontend>
    <events>
      <page_block_html_topmenu_gethtml_before>
        <observers>
          <CmsLinks_Model_Observer>
            <type>singleton</type>
            <class>graydog_cmslinks/observer</class>
            <method>addTopmenuLinks</method>
          </CmsLinks_Model_Observer>
        </observers>
      </page_block_html_topmenu_gethtml_before>
    </events>
  </frontend>
</config>

Update the observer model:

app/local/GrayDog/CmsLink/Model/Observer.php
class Graydog_CmsLinks_Model_Observer extends Varien_Object
{

  public function addTopmenuLinks (Varien_Event_Observer $observer) 
  {
    // Look for CMS pages with 'add to topmenu' set to true
    $cms_collection = Mage::getModel('cms/page')->getCollection()->addStoreFilter(Mage::app()->getStore()->getId());
    $cms_collection->getSelect()->where('include_in_nav = 1')->order('include_in_nav_order');

    if (!empty($cms_collection)) {
      $currUrlKey = Mage::getSingleton('cms/page')->getIdentifier();
      // Get topmenu from observer
      $topmenu = $observer->getMenu();
      foreach ($cms_collection as $page) {
        $page_data = $page->getData();
        $data = array(
          'name' => $page_data['title'],
          'id' => 'cms-page-'.$page_data['page_id'],
          'url' => Mage::getUrl(null, array('_direct' => $page_data['identifier'])),
          'is_active' => ($page_data['identifier'] == $currUrlKey) ? true : false
        );
        $new_node = new Varien_Data_Tree_Node($data, 'id', $topmenu->getTree(), $topmenu);
        $topmenu->addChild($new_node);
      }
    }
    return false;
  }
.....

This is pretty simple. Query for CMS pages where included_in_nav is true, create a new Varien_Data_Tree_Node object with the data and append it to the $topmenu object which is a Varien_Data_Tree. We also get the identifier of the current page and compare if to the menu item page's identifier to determine id is_active shoud be true or false. This adds the 'active' class for the current page in the menu. That's it. Now edit some CMS pages to include them in the nav and refresh.

Note If you're using the Wordpress integration extension I mentioned early, its observer will run before the local modules', which means any links added by it will appear first in the nav. If this is not the desired configuration it can be fixed by updating the extensions configuration for it depends on your module. In app/etc/modules/Fishpig_Wordpress.xml:

<?xml version="1.0"?>
<config>
  <modules>
    <Fishpig_Wordpress>
      <active>true</active>
      <codePool>community</codePool>
      <depends>
        <Graydog_CmsLinks />
      </depends>
    </Fishpig_Wordpress>
.......