Rendering Page Programmatically To Create Permanent URLs like /shop, /gallery

In this post, we cover how to render a page programmatically in a context of practical need to have a permanent generic URLs for widgets such us ‘Shop'(i.e. /shop), ‘Gallery'(i.e./gallery) etc. One may argue that by creating these permanent generic URLs you are limiting others because those URLs are not available unless the module implementing them is disable, however.

We believe permanent URLs can help people get up Drupal Site for the first time much faster and more convenient. We also believe it can be a nice entry point for your custom plugin or feature. At last, current stable version of feature modules doesn’t support URL import/export for dynamic URLs but works for permanent URLs

1. Create Permanent Generic URL

To create permanent URL, we use hook_menu() as follows:

function MODULE-NAME_theme_menu() {
    $items['basic-page'] = array(
        'title' => 'Basic Page',
        'page callback' => 'display_basic_page',
        'access arguments' => array('access content'),
        'type' => MENU_NORMAL_ITEM,
        'file' => 'inc/basic_page.pages.inc',
    );

    return $items;
}

This creates a url ‘/basic-page’ and once requested the callback function ‘display_basic_page’ is called. This callback function will call our custom theme function to generate the content:

2. Declare Callback and Custom Theme Functions

First, lets declare the callback function in file inc/basic_page.pages.inc file as follows:

function display_basic_page(){
 //query for the first instance for the basic_page
 $nid_basic_page = db_select('node', 'n')
        ->fields('n', array('nid'))
        ->fields('n', array('type'))
        ->condition('n.type', 'basic_page')
        ->range(0,1)
        ->orderBy('nid', 'DESC')
        ->execute()
        ->fetchCol();


    if(isset($nid_basic_page[0]) && is_numeric($nid_basic_page[0])){
        //prepare content for rendering
       @ToDO retrieve all contexts that apply for the current content type and pass it to init_content to activate them
       init_content($nid_basic_page[0], 'some_context_id');
    }

    return array(
            '#theme' => 'render_basic_page',
            '#view_mode' => 'full',
            '#type' => 'page',
        );
}

/***
 * Prepares node content for rendering including set in cache, enable context,etc
 * This is intended for building single point entry for widgets as described in blog post
 * http://margotskapacs.com/2014/02/rendering-page-programmatically-to-create-permanent-urls-like-shop-gallery
 * @param $node_id
 *      the id of node
 * @param string $context_id
 *      the id of context ot activate
 */
function init_content($node_id, $context_id = 'state_wide'){
    //load node
    $node_to_render = node_load($node_id);

    $content = node_view($node_to_render, 'full');

    //this seems to be standard Drupal content setup, so to be consistent
    $nodes = array(
        'nodes' => array(
            $node_to_render->nid => $content,
            '#sorted' => true,
        )
    );

    //set content in cache so its available for others
    drupal_set_page_content($nodes);

    //load context after content cache is set, so hook_context_load_alter receives the content
    if(module_exists('context')){
       //retrieve active contexts
        $all_active_contexts  = context_active_contexts();
        foreach($all_active_contexts as $key => $context){
            context_set('context', $context->name, $context);
        }
//        context_flush_caches();
    }else{
        watchdog(WATCHDOG_NOTICE, 'The module - context is not present. Unable set context with id ' . $context_id);
    }
}

First, the node instance is queried from database. Afterwards, this node is set in content cache. Next, we activate any context for this page. At last, our custom theme function ‘render_basic_page’ is specified to generate content. Next, lets declare our theme function:

 
function MODULE-NAME_theme(){
    $current_module = basename(__FILE__, '.module');
    $current_theme = $GLOBALS['theme'];


    return array(
        'render_basic_page'=> array(
            'template' => 'page',
            'render element' => 'page',
            'path' => drupal_get_path('theme',$current_theme).'/templates',
            'preprocess functions' => build_page_preprocessors('render_contact', $current_module)
        ),
    );
}

/***
 * builds list of default theme and all modules page preprocessors including one custom specific to the theme function
 * This is intended for building single point entry for widgets as described in blog post
 * http://margotskapacs.com/2014/02/rendering-page-programmatically-to-create-permanent-urls-like-shop-gallery
 * @param $theme_func_name
 *      the name of theme function the list of hooks are assigned
 * @param $module_name
 *      the module name implementing theme function
 * @return array
 *      return an array of all theme hooks in an order of execution consistent with Drupal order
 */
function build_page_preprocessors($theme_func_name, $module_name){
    $current_theme = $GLOBALS['theme'];

    $prefix_processors =  array(
        'template_preprocess',
        'template_preprocess_page',
        $module_name . '_preprocess_'.$theme_func_name,
    );
    $post_processors = array(
        $module_name . '_preprocess_page',
        $current_theme . '_preprocess_page',
    );
    $other_module_preprocessors  = module_preprocessors('preprocess_page');
    $combined_processors  = array_merge($prefix_processors,$other_module_preprocessors,$post_processors);

    return $combined_processors;
}

/**
 * retrieves list of functions with hook provided
 * @param $hook
 *   hook name (i.e. preprocess_page, preprocess_block)
 * @return
 *   list of hook functions in format of module_hook
 */
function module_preprocessors($hook){
    $preprocessors = array();

    //retrieve all modules with the hook - preprocess_page
    $modules = module_list();
    foreach($modules as $module){
        if (module_hook($module, 'preprocess_page')) {
            $preprocessors[] = $module.'_preprocess_page';
        }
    }

    return $preprocessors;
}

Here, the custom theme function ‘render_basic_page’ is declared. We also specify the template file and path to it to used for rendering content. In addition, we specify the name of variable(i.e. ‘page’ which is conventional variable name for Drupal) where to put the Drupal generated variables available in preprocessor functions.

At last, we set all page preprocessors. We have externalized into function build_page_preprocessors to build a list of all preprocessors. Besides other module page preprocess, there are 5 total preprocessors. The ‘template_preprocess_page’ & ‘template_preprocess’ are the functions that sets default variables such as ‘classes_array’, ‘site_name’,etc. The ‘MODULE-NAME_preprocess_THEME-FUNC-NAME’ preprocessor function is the default preprocessor function called if no preprocessor functions are set. We use it for logic specific to the theme_function that is mostly used to load the node to render. Afterwards, all other module preprocessors are included. Afterwards, the preprocess function – ‘MODULE-NAME_preporcess_page’ is where the module specific logic is implemented. As the last, we specify themes preprocess function, so the themer can decide last on how the variables are presented and customize as they wish

3. Preprocess Node Instance For Rendering

We have to specify the content to render on this page that we do in the preprocess function. Every custom theme function, by default have a preprocessor function ‘hook_preprocessor_NAME-OF-THEME-FUNC’ that we use in this case to process content:

function MODULE-NAME_preprocess_render_basic_page(&$vars){
    //retrieve current node being loaded(alternative is to get the node from content cache)
    $current_node = array_slice($vars['page']['content']['system_main']['nodes'],0,1);
    
    //set content
    if(isset($current_node) && !empty($current_node)){
        //prepare for theme_preprocess_page function
        $vars['node'] = array_values($current_node)[0]['#node'];
    }else{
          drupal_set_message(t('It appears there is no Basic Page created. Please, go to \'Content\'->\'Add Content\'->\'Basic Page\' or import sample data'));
        $vars['page']['content'] = array(
            '#markup' => "" ,
        );
    }

Here, first we retrieve the current node to be rendered. This can be done from content cache or page structure as in our case. At last, we set the $vars[‘node’] to the node we rendering so the other preprocessor functions have it available. In case, there are no nodes available to render, then we display message.

Templates

With this approach the Page scope template is not set and The Node scope template is the default. In case, you would like to use your own, then one way to configure each would be via preprocess hooks as following:

function MODULE-NAME_preprocess_node(&$vars){
    if(arg(0) == 'basic-page'){
        $vars['theme_hook_suggestions'][] = 'node__basic_page_no_wrapper';
    }
}

function MODULE-NAME_preprocess_page(&$vars){
    if(arg(0) == 'basic-page'){
        $vars['theme_hook_suggestions'][] = 'page__basic_page_widget';
    }
}

In the custom theme function, we specified the preprocessor hooks including -MODULE-NAME_preprocess_node and MODULE-NAME_preprocess_page, so they are called. Here, in the first hook we set the node template to be ‘node–basic-page-no-wrapper.tpl.php’ and then in the second hook the page template is set to be ‘page–basic-page-widget.tpl.php’ based on the path(i.e. arg(0)).

Summary

In this post, we covered the basic page scenario but this can be useful for more complicated cases like a ‘shop’, ‘gallery’, etc for plugins that you like to provide to your users out of box with little or minimal configurations. All they have to do is go to the generic permanent URL.

Troubleshooting

1. Error message $breadcrumb or $message are not set

The variables – $breadcrumb and $message is set in processor function template_process_page. Make sure this function is specified when declaring your custom theme function:

function HOOK_theme($existing, $type, $theme, $path){
  return array( 
        ...     
       'render_portfolio' => array(
            'template' => 'page',
            'render element' => 'page',
            'path' => $current_theme . '/templates',
            'preprocess functions' => build_page_preprocessors('render_portfolio', $current_module),
            'process functions' => array('template_process','template_process_page'),
        ),
);
}

This will set template_process_page function to be executed and, thus, set $message and $breadcrumbs variables

Reference:

http://chicago2011.drupal.org/sessions/render-api-drupal-7