background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this tutorial I'll explain how to setup your Drupal site to be mobile friendly. Before you begin, it's helpful to consider the following: 1) which mobile devices to support; 2) using a different theme for mobile; 3) which hostnames will be used; 4) multi-site configuration options; and 5) any site alterations to simplify the mobile experience.

For my site, I decided on the following:
1) iPhones (to start)
2) Use a more clean theme for my mobile site: DevSeed's Singular
3) redirect mobile traffic to a new subdomain (mobile.ericlondon.com)
4) I was using sites/default for my blog, so hosting another hostname on the same filesystem was not an issue
5) I decided to remove some items in my primary navigation, and add some jQuery (accordion effect) to collapse node content on my front page.

I added the new DNS for my subdomain to point to the same IP address of my main site. In a standard Apache vhosts configuration, you can add a ServerAlias directive to ensure the mobile hostname is handled by the main site's vhost. For example:

<VirtualHost *:80>
  ServerName ericlondon.com
  ServerAlias mobile.ericlondon.com
  DocumentRoot /var/www/vhosts/ericlondon.com/httpdocs
</VirtualHost>

I installed and enabled the Singular theme in sites/all/theme/singular.

I added some mobile specific configurations in my settings.php file (sites/default/settings.php):

<?php
/**
* Mobile Theme Configuration
*/

// define mobile http host
define('MOBILE_HTTP_HOST', 'mobile.ericlondon.com');

// define mobile theme
define('MOBILE_THEME','singular');

// override custom theme for mobile site
if ($_SERVER['HTTP_HOST'] == MOBILE_HTTP_HOST) {
 
$GLOBALS['custom_theme'] = MOBILE_THEME;
}

// check for iPhone
$is_iphone = preg_match('/iphone/i', $_SERVER['HTTP_USER_AGENT']); 

// redirect to mobile theme
if ($is_iphone && $_SERVER['HTTP_HOST']!=MOBILE_HTTP_HOST) {
 
header('Location: http://' . MOBILE_HTTP_HOST . $_SERVER['REQUEST_URI']);
  die;
}
?>

Now, if someone visits my site using an iPhone, the user would be redirected to my specified mobile address, AND a new mobile theme would be used!

Mobile iPhone

In addition, I decided to make some alterations to my mobile theme to simplify the interface. I created a module and added a hook_preprocess_page() implementation:

<?php
function MYMODULE_preprocess_page(&$vars) {

 
// only process page variables if this is the mobile address
 
if ($_SERVER['HTTP_HOST'] != MOBILE_HTTP_HOST) {
    return;
  }

 
// define mobile javascript
  /*
  NOTE: this jQuery is very specific to my theme, and is just shown as an example.
  I looked into using jquery_ui's accordion library, but it would not work out of the box with the structure of my new mobile theme :(
  In an ideal situation, all jQuery would be put in separate include files
  */
 
$js = "
 
    // define node container
    var node_container = '.front #page #content';
     
    // define function to collapse node content
    function collapse_nodes() {     
      $(node_container + ' .node div').hide();
    }
 
    // define function to add click event
    function node_title_add_click() {
      $(node_container + ' .node h2.node-title a').click(function(){
        collapse_nodes();
        $('div', $(this).parent().parent()).show();
        return false;
      });
    }
   
    $(document).ready(function(){

      collapse_nodes();
      node_title_add_click();
     
    });
  "
;
 
drupal_add_js($js, 'inline');
 
 
// rebuild scripts variable
 
$vars['scripts'] = drupal_get_js();
 
 
// determine a list of hrefs to remove from primary navigation
 
$href_remove = array(
   
'drupal',
   
'tagadelic/chunk/1',
   
'recent-posts',
   
'rss.xml',
   
'logout',
   
'contact',
  );
 
 
// loop through primary links and remove as necessary
 
if (is_array($vars['primary_links'])) {
    foreach (
$vars['primary_links'] as $key => $value) {
      if (
in_array(strtolower($value['href']), $href_remove)) {
        unset(
$vars['primary_links'][$key]);
      }
    }
  }
 
}
?>

By added the above module code and jQuery, I removed some items from my primary navigation and added an accordion-like interface for the front page:

Mobile theme

NOTE: If you're using a Mac, the iPhone Simulator application (which comes with Xcode + iPhone SDK) is a great way to development and test mobile configurations.In this tutorial I'll explain how to setup your Drupal site to be mobile friendly. Before you begin, it's helpful to consider the following: 1) which mobile devices to support; 2) using a different theme for mobile; 3) which hostnames will be used; 4) multi-site configuration options; and 5) any site alterations to simplify the mobile experience.

For my site, I decided on the following:
1) iPhones (to start)
2) Use a more clean theme for my mobile site: DevSeed's Singular
3) redirect mobile traffic to a new subdomain (mobile.ericlondon.com)
4) I was using sites/ericlondon.com for my blog, so hosting another hostname on the same filesystem was not an issue
5) I decided to remove some items in my primary navigation, and add some jQuery (accordion effect) to collapse node content on my front page.

I added the new DNS for my subdomain to point to the same IP address of my main site. In a standard Apache vhosts configuration, you can add a ServerAlias directive to ensure the mobile hostname is handled by the main site's vhost. For example:

<VirtualHost *:80>
  ServerName ericlondon.com
  ServerAlias mobile.ericlondon.com
  DocumentRoot /var/www/vhosts/ericlondon.com/httpdocs
</VirtualHost>

I installed and enabled the Singular theme in sites/all/theme/singular.

I added some mobile specific configurations in my settings.php file (sites/ericlondon.com/settings.php):

<?php
/**
* Mobile Theme Configuration
*/

// define mobile http host
define('MOBILE_HTTP_HOST', 'mobile.ericlondon.com');

// define mobile theme
define('MOBILE_THEME','singular');

// override custom theme for mobile site
if ($_SERVER['HTTP_HOST'] == MOBILE_HTTP_HOST) {
 
$GLOBALS['custom_theme'] = MOBILE_THEME;
}

// check for iPhone
$is_iphone = preg_match('/iphone/i', $_SERVER['HTTP_USER_AGENT']); 

// redirect to mobile theme
if ($is_iphone && $_SERVER['HTTP_HOST']!=MOBILE_HTTP_HOST) {
 
header('Location: http://' . MOBILE_HTTP_HOST);
  die;
}
?>

Now, if someone visits my site using an iPhone, the user would be redirected to my specified mobile address, AND a new mobile theme would be used!

Mobile iPhone

In addition, I decided to make some alterations to my mobile theme to simplify the interface. I created a module and added a hook_preprocess_page() implementation:

<?php
function MYMODULE_preprocess_page(&$vars) {

 
// only process page variables if this is the mobile address
 
if ($_SERVER['HTTP_HOST'] != MOBILE_HTTP_HOST) {
    return;
  }

 
// define mobile javascript
  /*
  NOTE: this jQuery is very specific to my theme, and is just shown as an example.
  I looked into using jquery_ui's accordion library, but it would not work out of the box with the structure of my new mobile theme :(
  In an ideal situation, all jQuery would be put in separate include files
  */
 
$js = "
 
    // define node container
    var node_container = '.front #page #content';
     
    // define function to collapse node content
    function collapse_nodes() {     
      $(node_container + ' .node div').hide();
    }
 
    // define function to add click event
    function node_title_add_click() {
      $(node_container + ' .node h2.node-title a').click(function(){
        collapse_nodes();
        $('div', $(this).parent().parent()).show();
        return false;
      });
    }
   
    $(document).ready(function(){

      collapse_nodes();
      node_title_add_click();
     
    });
  "
;
 
drupal_add_js($js, 'inline');
 
 
// rebuild scripts variable
 
$vars['scripts'] = drupal_get_js();
 
 
// determine a list of hrefs to remove from primary navigation
 
$href_remove = array(
   
'drupal',
   
'tagadelic/chunk/1',
   
'recent-posts',
   
'rss.xml',
   
'logout',
   
'contact',
  );
 
 
// loop through primary links and remove as necessary
 
if (is_array($vars['primary_links'])) {
    foreach (
$vars['primary_links'] as $key => $value) {
      if (
in_array(strtolower($value['href']), $href_remove)) {
        unset(
$vars['primary_links'][$key]);
      }
    }
  }
 
}
?>

By added the above module code and jQuery, I removed some items from my primary navigation and added an accordion-like interface for the front page:

Mobile theme

NOTE: If you're using a Mac, the iPhone Simulator application (which comes with Xcode + iPhone SDK) is a great way to development and test mobile configurations.

I created a Drupal site to host my photography in CCK Imagefield nodes and used Lucene to enhance my search functionality. By default Drupal's search results are text-based so I decided to add some code to show image thumbnails in my search results. I checked out Drupal Lucene's hooks and decided to implement a hook_luceneapi_result_alter() function in my existing module.

<?php
function MYMODULE_luceneapi_result_alter(&$result, $module, $type = NULL) {
 
 
// check for node results
 
if ($type == 'node') {
 
   
// check node type
   
if ($result['node']->type == 'image') {
   
     
// define an imagecache image path for image thumbnail
     
$imagecache_path_thumbnail = file_directory_path() . '/imagecache/thumbnail' . str_replace(file_directory_path(),'',$result['node']->field_image[0]['filepath']);     
     
     
// define an imagecache image path for image (large)
     
$imagecache_path_large = file_directory_path() . '/imagecache/large' . str_replace(file_directory_path(),'',$result['node']->field_image[0]['filepath']);
   
     
// define theme_image() variables
     
$alt = check_plain($result['node']->title);
     
$title = check_plain($result['node']->title);
     
// add rel=lightbox to enable lightbox2 module
     
$attributes = array(
       
'rel' => 'lightbox',
      );
     
// let imagecache define the size
     
$getsize = FALSE;
     
// generate the image hml
     
$image_html = theme('image', $imagecache_path_thumbnail, $alt, $title, $attributes, $getsize);     
   
      if (
$image_html) {
               
       
// define lightbox link
       
$image_link = l(
         
$image_html,
         
$imagecache_path_large,
          array(
           
'html' => true,
           
'attributes' => array(
             
'rel' => 'lightbox',
            )
          )
        );

       
// add data to the result variable, passed by reference
       
$result['image_thumbnail'] = $image_link;
       
      }
   
    }
 
  }

}
?>

The above code adds additional data to my search results variables. I then implemented a hook_preprocess_search_result() function in my theme's template.php file to pass this data to the search-result.tpl.php template file.

<?php
function MYTHEME_preprocess_search_result(&$variables) {

 
// ...snip...

  // check for lucene node search results
 
if ($variables['type']=='luceneapi_node') {

   
// check for image
   
if ($variables['result']['image_thumbnail']) {   

     
// pass additional data to theme template file
     
$variables['image_thumbnail'] = $variables['result']['image_thumbnail'];

    }
   
  }

}
?>

And in my theme's search-result.tpl.php template file, I added the following PHP to show the new variable.

<div class="search-result <?php print $search_zebra; ?>">

  <?php if($image_thumbnail): ?>
    <?php print $image_thumbnail; ?>
  <?php endif; ?>

  <!-- ...snip... -->

I also added a few lines of CSS in my theme's style.css file to tidy up the layout.

.search-results.luceneapi_node-results .search-result {
  clear: both;
}

.search-results.luceneapi_node-results .search-result img {
  float: left;
  margin: 0px 20px 20px 0px;
}

The visual results can be seen here on my photo gallery.

Visual search results

I recently signed up for Google AdSense and I thought it would be fun to show ads for Internet Explorer users. Of course, you could use this tutorial for more productive purposes. Here is how I integrated this functionality into my theme.

I added 2 regions to THEME.info file:

regions[ad_left] = Ad Left
regions[ad_right] = Ad Right

I then included the regions in my theme by adding some code to my theme's page.tpl.php file:

<?php if ($ad_left): ?>
  <div id='ad_left' class='google_ads'>
    <?php print $ad_left; ?>
  </div>
<?php endif; ?>

<?php if ($ad_right): ?>
  <div id='ad_right' class='google_ads'>
    <?php print $ad_right; ?>
  </div>
<?php endif; ?>

Next, I added some CSS in my style.css theme file to absolutely position the regions in a fixed position:

.google_ads {
  position: absolute;
  bottom: 10px;
  width: 120px;
  height: 600px;
  display: none;
}

.google_ads .block {
  padding: 0;
  margin: 0;
}

body > .google_ads {
  position: fixed;
}

#ad_left {
  left: 10px; 
}

#ad_right {
  right: 10px;
}

Now that my regions were setup, I added 2 blocks containing the Google AdSense code and assigned the blocks to my new regions. You'll need to make sure the input filter you choose allows for javascript to be included.

At this point, the ads are included for every browser, but they will not be shown due to the "display: none" property I added to the "google_ads" CSS class. I added a few lines of jQuery in my theme's script.js file to show the regions for IE users:

$(document).ready(function(){
  // per IE ads
  if ($.browser.msie) {
    $('.google_ads').show();  
  }
});

If you view this page in IE, you'll see my code in action ^_^

My additional thoughts... this code executes the Google AdSense javascript regardless of your browser, but it is only shown to IE users. A more efficient method would be to use AJAX to include the javascript ONLY if the browser is IE, or of course, by implementing a server side browser detection library.

In this blog entry, I'll show you how you can add some module code to allow users to select different themes for their profile page. I decided to use the standard user-profile.tpl.php as the base template. I copied this file into my theme folder and replicated it a few times. I named the files:

user-profile-version-1.tpl.php
user-profile-version-2.tpl.php

For my example, I simply added the text "Version 1" and "Version 2" to the top of these files to show it's working, but you could revise the layout, add CSS, etc.

Next, I created a module to contain all the following code. I defined a hook_perm() and hook_menu() function to add a menu local task (tab) to the user page.

<?php
function MYMODULE_perm() {
  return array(
   
'choose profile theme'
 
);
}

function
MYMODULE_menu() {
 
 
$items = array();
 
 
$items['user/%user/choose-profile-them'] = array(
   
'title' => 'Choose Profile Theme',
   
'page callback' => '_MYMODULE_callback_choose_profile_theme',
   
'page arguments' => array(1),
   
'access callback' => 'user_access',
   
'access arguments' => array('choose profile theme'),
   
'type' => MENU_LOCAL_TASK,
  );
 
  return
$items;
 
}
?>

user profile tab

Next, I defined the page callback to create a list of available templates for the user to choose from.

<?php
function _MYMODULE_callback_choose_profile_theme($user) {

 
// create an empty strong variable for page html
 
$html = "";

 
// define a list of available profile templates
 
$profileTemplates = array(
   
'user-profile-version-1' => 'Version 1',
   
'user-profile-version-2' => 'Version 2'
 
);
 
 
// loop through templates and ensure they exist
 
foreach ($profileTemplates as $template => $name) {
    if (!
file_exists(path_to_theme() . '/' . $template . '.tpl.php')) {
      unset(
$profileTemplates[$name]); 
    }
  }
 
  if (
count($profileTemplates)) {
   
$html .= drupal_get_form('_MYMODULE_callback_choose_profile_theme_form', $user, $profileTemplates);
  } else {
   
$html .= "No profile themes currently exist.";
  }
 
  return
$html;
 
}
?>

The following functions define the form array, and validation and submit handlers. When the form is submitted, the template option is saved to the user account variables using the user_save() function.

<?php
function _MYMODULE_callback_choose_profile_theme_form($form_state, $user, $profileTemplates) {

 
$form = array();
 
 
// add a select input element
 
$form['profileTemplate'] = array(
   
'#type' => 'select',
   
'#title' => t('Profile Template'),
   
'#options' => $profileTemplates,
   
'#default_value' => $GLOBALS['user']->profileTemplate
 
);
 
 
// add a submit button
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit')
  );
 
 
// add hidden element for userID
 
$form['userID'] = array(
   
'#type' => 'hidden',
   
'#value' => $user->uid
 
);
 
  return
$form;
}

function
_MYMODULE_callback_choose_profile_theme_form_validate($form, &$form_state) {
 
// TODO: add your additional form validation here
 
  // ensure userID submitted matches current user
 
if ($form_state['values']['userID']!=$GLOBALS['user']->uid) {
   
form_set_error('', t('Error processing form.')); 
  }
 
}

function
_MYMODULE_callback_choose_profile_theme_form_submit($form, &$form_state) {

 
// load user object
 
$user = user_load($form_state['values']['userID']);
 
 
// save profile template to user variables
 
user_save($user, array('profileTemplate' => $form_state['values']['profileTemplate']));
 
 
// set a message
 
drupal_set_message('Your profile template has been set.');
 
}
?>

user profile form

Last, I defined the preprocess function to see if a template has been set in the user variables, and add the templates suggestion to change the user profile template.

<?php
function MYMODULE_preprocess_user_profile(&$variables) {

 
// ensure the template file exists, and is set in the user object
 
if (file_exists(path_to_theme() . '/' . $variables['user']->profileTemplate . '.tpl.php') && $variables['user']->profileTemplate) {

   
// set a templates suggestion
   
$variables['template_files'][] = $variables['user']->profileTemplate;
  }
 
}
?>

As you can see below, when I choose the "Version 2" template option, the file "user-profile-version-2.tpl.php" is being loaded, and the text "Version 2" is displayed at the top of the page.

user profile selected

Back in March I wrote this article: Displaying the total number of results in a view and how many are being shown on the current page. This code snippet will show you how you can add the same functionality on the search results page and improve usability by showing the following text:

Displaying ### - ### of ### results

First I added a preprocess_search_results function in my theme's template.php file to generate the html to show on the search results page:

<?php
function MYTHEME_preprocess_search_results(&$variables) {

 
// define the number of results being shown on a page
 
$itemsPerPage = 10;

 
// get the current page
 
$currentPage = $_REQUEST['page']+1;

 
// get the total number of results from the $GLOBALS
 
$total = $GLOBALS['pager_total_items'][0];
   
 
// perform calculation
 
$start = 10*$currentPage-9;
 
$end = $itemsPerPage * $currentPage;
  if (
$end>$total) $end = $total;
   
 
// set this html to the $variables
 
$variables['MYTHEME_search_totals'] = "Displaying $start - $end of $total results";

}
?>

Now, you can show this variable on the search results page by copying the search results template file (modules/search/search-results.tpl.php) into your theme folder and editing it.

Here is the new contents of my modified file (minus the comments at the top):

<!-- NEW SEARCH RESULTS TOTALS: -->
<?php print $MYTHEME_search_totals; ?>

<!-- ORIG CONTENTS: -->
<dl class="search-results <?php print $type; ?>-results">
  <?php print $search_results; ?>
</dl>
<?php print $pager; ?>

Syndicate content