In order to launch an inexpensive global network of sites run by volunteers that would provide shoppers categorized directories to local stores, I needed to leverage the power of WordPress with some unusual customizations and workarounds. There are a number of approaches to providing local experiences with WordPress. In this tutorial, I’m going to show you why and how we decided to make a customized solution.
In July, I launched a global guide to Internet-based alternatives to shopping at Amazon called FleetheJungle.com and the response was excellent. We received a lot of media coverage and subsequent traffic. It’s not a database-driven app; instead we leveraged the outstanding KnowHow knowledge base theme:
But to grow the site, we needed to offer geolocal versions for individual cities. Certainly the global site helps visitors cancel their prime memberships or find alternatives to Kindle books but there was nothing to encourage people to support small businesses in their communities.
To begin, we’d build a guide for local shoppers in nearby Portland, a city with a proud heritage of quirky, independent small businesses.
The Technical Challenge
Certainly, I’ve written enough about quickly launching a plethora of WordPress websites using pre-configured, pre-optimized recipes, but maintaining related requirements for a network can be extremely time-consuming—things like managing SEO, WordPress and plugin upgrades, etc.
I also considered WordPress Multisite but even though I know she’s grown a lot, we had a number of awkward difficult pairings and were probably never meant to be together. Once you get involved with “her”, it’s very hard to break up.
I needed a simpler solution.
In this tutorial, I’ll guide you through customizations and tricks I used with WordPress and my knowledge base theme to launch our Portland shopping guide and make it easy to launch any number of other cities—while avoiding the overhead of managing dozens or hundreds of sites.
The Geolocal Network Requirements
Here are some of the basic requirements for launching more sites:
1. Maintain a worldwide site. I wanted to keep the global, fleethejungle.com primary domain intact, free of local articles but instead a guide to our available cities.
2. Localize by sub-domain. I wanted content localized by sub-domain. For example, a visit to http://portland.fleethejungle.com would promote local content on the site while re-using some specific national site categories related to shopping on the broader Internet, e.g. video streaming (there’s no Portland-based alternative to HBO Now). CraigsList provides a good model for this.
3. Integration between local and worldwide. I wanted to provide local visitors an explanation of the differences between their local site and the global site while encouraging them to participate in helping improve the listings in their city.
4. Volunteer-managed. I wanted to make it easy to regularly add cities to the site in a way that would only require volunteers with basic blogging skills to launch and maintain. There are too many requirements to launching new WordPress sites for each individual city to easily do this with volunteers.
5. Minimal maintenance. Finally, I wanted to keep the maintenance of the site network as simple as running a single site.
Let’s explore some of the technical approaches that are often used to address these kinds of requirements.
Technical Approaches
For content management, there are three basic approaches I thought of:
- Run different installations of WordPress.
- Use WordPress Multisite to run various sites.
- Customize WordPress to dynamically filter content for geography.
As I said earlier, I’m not a fan of Multisite and I wanted to avoid the complexity of launching and maintaining a variety of WordPress sites. A site like Flee the Jungle would have so much central content that would need to be re-used across city pages (such as how to terminate your Prime membership), and I didn’t want to have to maintain this content across different WordPress installations—or write code to do so.
I decided to filter the content depending on the domain or subdomain used by the visitor in the browser.
If you’ve read my tutorial How to Use Zillow Neighborhood Maps and HTML5 Geolocation, you know how to use HTML5 browser-based geolocation for determining where a user is. However, I wanted the user to have more control.
I wanted the user to use a sub-domain like the one that’s worked for years with CraigsList to guide them to their city.
But WordPress’s dominion over absolute URLs makes this difficult. When I first began experimenting with tracking the inbound sub-domain for a city and mapping it to links on the page, I ran into WordPress’s habit of creating absolute links nearly everywhere. A colleague I spoke to about it laughed immediately when I asked him about it—it’s a common foe of WordPress developers.
In this tutorial, I’ll share the customizations within our theme’s PHP-based queries to build our city-based sites and how we ultimately worked around the absolute URL problem.
Customizing WordPress for Geolocal
Here’s how I decided to implement city-based sites for Flee the Jungle that could be easily maintained and authored by local bloggers without additional technical skills.
Leveraging Tagging With Sub-Domains
Articles written for cities would be tagged by city name, e.g. “portland”. Articles for the global site would be tagged “worldwide”. I wanted the ability to include worldwide articles about Internet-based stores for some categories where there was less effective local support such as video streaming. Other categories are certainly appropriate to focus exclusively on local stores such as Groceries, Auto Stores and Toy Stores.
A City Site’s Geolocal Home Page
For geolocal sites, we keep the worldwide categories in place and only show different articles for the locally appropriate categories. In the future, we’ll append worldwide articles to the local choices within a category when appropriate.
When visitors view category pages and search, we also filter and customize the results based on these design choices.
Defined Variables
To do this, we define arrays within the theme by their IDs:
- list of categories for worldwide
- list of categories for local cities
- list of active cities and their subdomain prefixes
Here are a few examples. First, here’s my array of worldwide (nonlocal) category IDs:
// Category IDs that are worldwide, digital to be displayed without changes // e.g. Getting Started, Prime and Smile Alternatives, Streaming Media, etc. $nonlocal_category_ids = [1,22,29,30,46,57,60,195];
You can retrieve those from peeking at the category ID in the dashboard category list (by hovering with the mouse over the category name as shown further below with tags):
Or, edit the category and grab the ID from the edit URL (Getting Started is 29):
Here are the tag IDs for Seattle and Portland:
// Tag IDs for city names e.g. Seattle, Portland $known_cities = array(49,211);
Seattle is 49. You can see the tag id by hovering over the city and letting the status bar reveal it:
Using the Sub-Domain to Geotarget
To obtain the domain or sub-domain, I used Trevor Scott’s WordPress tip and placed it within my theme’s functions.php.
Here’s the WordPress Dashboard Theme Editor:
And, here’s Trevor’s code:
/** * @author Trevor Scott <[email protected]> * @version 1.0 2010-12-07 *https://wordpress.org/support/topic/how-do-i-get-sub-domain-name * Grab the subdomain portion of the URL. If there is no sub-domain, the root * domain is passed back. By default, this function *returns* the value as a * string. Calling the function with echo = true prints the response directly to * the screen. * * @param bool $echo */ function arrested_subdomain($echo = false) $hostAddress = explode ( '.', $_SERVER ["HTTP_HOST"] ); if (is_array ( $hostAddress )) if (eregi ( "^www$", $hostAddress [0] )) $passBack = 1; else $passBack = 0; if ($echo == false) return ($hostAddress [$passBack]); else echo ($hostAddress [$passBack]); else return (false);
Global Home Page
When visitors arrive at the global home page, the domain will be fleethejungle.com rather than a sub-domain. I’ve created another function, configure_geolocal()
, which is called at the beginning of the template header.php file. Here’s the first part of the function:
function configure_geolocal() { global $nonlocal_category_ids; global $known_cities; global $domain_locale; global $logo_image_url; // Tag IDs for city names e.g. Seattle, Portland $known_cities = array(49,211); // Category IDs that are worldwide, digital to be displayed without changes // e.g. Getting Started, Prime and Smile Alternatives, Streaming Media, etc. $nonlocal_category_ids = [1,22,29,30,46,57,60,195]; $domain_locale = arrested_subdomain(); if ($domain_locale == 'www' or $domain_locale == 'fleethejungle') { $domain_locale = false;
Here’s how it’s called by get_header()
within the theme’s header.php:
<?php configure_geolocal(); ?> <!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta charset="<?php bloginfo( 'charset' ); ?>" /> <meta content="width=device-width,minimum-scale=1,maximum-scale=1" name="viewport"> <meta name="format-detection" content="telephone=no"> <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
When the domain is global, $domain_locale
will be false. Under these circumstances, I will exclude articles tagged locally by cities. And I will do this on the home page and the category page. When the domain refers to a local city, $domain_locale
will represent the prefix, e.g. seattle or portland.
In the theme’s home page, if the visitor is on the worldwide site (root domain), I exclude articles tagged with cities from the $st_cat_post_args
array. However, if the visitor is on a city page and the sub-domain is known, we only display local articles in most categories except those identified for nonlocal_category_ids—those are the ones more appropriate for non-local content such as video streaming:
global $known_cities; global $nonlocal_category_ids; global $domain_locale; if ($domain_locale===false) // root visit $st_cat_post_args['tag__not_in']=$known_cities; else { // local city being viewed but include worldwide articles in this category if (in_array($st_category->term_id,$nonlocal_category_ids) $st_cat_post_args['tag_slug__in']=array($domain_locale,'worldwide'); else // only show local articles $st_cat_post_args['tag_slug__in']=array($domain_locale);
Here’s the code within the context of more of the function:
//List Posts $st_cat_post_num = of_get_option('st_hp_cat_postnum'); $st_posts_order = of_get_option('st_hp_cat_posts_order'); global $post; // If show posts is 0 do nothing if ($st_cat_post_num != 0) { // Listed by popular? if ($st_posts_order == 'meta_value_num') $st_cat_post_args = array( 'numberposts' => $st_cat_post_num, 'orderby' => $st_posts_order, 'meta_key' => '_st_post_views_count', 'category__in' => $st_category->term_id ); else $st_cat_post_args = array( 'numberposts' => $st_cat_post_num, 'orderby' => $st_posts_order, 'category__in' => $st_category->term_id ); global $known_cities; global $nonlocal_category_ids; global $domain_locale; if ($domain_locale===false) // root visit $st_cat_post_args['tag__not_in']=$known_cities; else { // local city being viewed but include worldwide articles in this category if (in_array($st_category->term_id,$nonlocal_category_ids) $st_cat_post_args['tag_slug__in']=array($domain_locale,'worldwide'); else // only show local articles $st_cat_post_args['tag_slug__in']=array($domain_locale); $st_cat_posts = get_posts($st_cat_post_args); echo '<ul class="category-posts">'; foreach($st_cat_posts as $post) : setup_postdata($post); ?> <?php // Set post format class if ( has_post_format( 'video' )) $st_postformat_class = 'video'; else $st_postformat_class = 'standard'; ?> <li class="format-<?php echo $st_postformat_class; ?>"><a href="<?php the_permalink(); ?>"> <?php the_title(); ?> </a></li> <?php endforeach; echo '</ul>';
Customizing the Behavior of Category Listing Pages
With the KnowHow theme, visitors may also click on the category headings, taking them to an article list within the category.
If they are on the global site, I want to show only global articles. I chose to tag all global articles as ‘worldwide’ to facilitate this.
So for the global site, I show only articles tagged ‘worldwide’ and for local city sites, I show only articles tagged for that city. If the category is an exceptional case for both, we include articles from both the current city and worldwide.
Here’s the code within the theme’s category.php:
<?php while ( have_posts() ) : ?> <?php the_post(); $show_post = false; if ($GLOBALS["domain_locale"]===false) // root worldwide visit, exclude local articles if (has_tag('worldwide')) $show_post=true; else // local city being viewed if (in_array($current_cat,$GLOBALS['nonlocal_category_ids'])) else // only show local articles if (has_tag($GLOBALS["domain_locale"])) $show_post=true; if ($show_post) get_template_part( 'content', get_post_format()); ?> <?php endwhile; ?>
I’m not customizing the query here because performance is currently not a major issue. I’m just skipping results out of context and not displaying them.
In the future, I may customize this further for city sites by displaying worldwide results at the bottom of the category page in a distinct section.
Customizing Search
It’s also important to perform these sorts of customizations for search. The KnowHow theme provides an AJAX search result or a generated page of results. We need to provide code to customize each option.
Live AJAX Search
For live search, there is no get_header()
call so we need to configure our location dynamically:
<?php if(!empty($_GET['ajax']) ? $_GET['ajax'] : null) { // Is Live Search ?> <?php // get geolocal settings for live search only configure_geolocal(); ?>
Then we integrate the same logic used on category pages within live search results:
// check if one of its categories is excluded from local $category_excluded=false; foreach((get_the_category()) as $category) if (in_array($category->cat_ID,$GLOBALS['nonlocal_category_ids'])) $category_excluded=true; break; if ($GLOBALS["domain_locale"]!==false && has_tag( $GLOBALS["domain_locale"]) || ($category_excluded && has_tag('worldwide'))) ?> <li class="<?php echo $st_search_class ?>"> <?php if ( 'st_faq' == get_post_type() ) ?> <a href="<?php echo home_url(); ?>/<?php echo $st_faq_slug ?>/#faq-<?php the_ID(); ?>"><?php the_title(); ?></a> <?php else ?> <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a> <?php ?> </li> <?php else ?> <?php if ($GLOBALS["domain_locale"]===false && has_tag( "worldwide")) ?> <li class="<?php echo $st_search_class ?>"> <?php if ( 'st_faq' == get_post_type() ) ?> <a href="<?php echo home_url(); ?>/<?php echo $st_faq_slug ?>/#faq-<?php the_ID(); ?>"><?php the_title(); ?></a> <?php else ?> <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a> <?php ?> </li> <?php ?> <?php ?>
Dynamic Search Results Page
If it’s not AJAX, then get_header()
is called for us and it’s a bit simpler:
<?php while ( have_posts() ) : the_post(); ?> <?php // check if one of its categories is excluded from local $category_excluded=false; foreach((get_the_category()) as $category) if (in_array($category->cat_ID,$GLOBALS['nonlocal_category_ids'])) $category_excluded=true; break; if ($GLOBALS["domain_locale"]!==false && has_tag( $GLOBALS["domain_locale"]) || ($category_excluded && has_tag('worldwide'))) get_template_part( 'content', get_post_format()); else if ($GLOBALS["domain_locale"]===false && has_tag( "worldwide")) get_template_part( 'content', get_post_format()); ?> <?php endwhile; ?>
As I mentioned above for Categories, I may decide to customize search results to display a second section of results for global articles on city sites.
Approaches to Content
To provide local visitors an explanation of the differences between their local site and the global site while encouraging them to participate in helping improve the listings in their city, I used a plugin I wrote about in January 2015 for Envato Tuts+: The Benefits of Using the Free Shortcoder Plugin. It’s a plugin that lets you use macros essentially to expand re-usable HTML content in place. For each city article, we’re using it to customize what local visitors see when they arrive from Portland.
Once the plugin was installed, I created a shortcode for each city such as [sc:pdx] which provides a simple blurb at the top of local article pages:
Then, whenever we added a local article, we included the shortcode [sc:pdx] at the top:
Here’s what the article looks like when visitors arrive:
Overcoming Absolute Links
I’m a long-time user of Digital Ocean but I’m becoming a more enthusiastic fan of WP Engine as well; well-managed hosting provides advantages in some circumstances. It turned out WP Engine offered a solution to the WordPress absolute URL problem which was better than alternatives.
WP Engine offers a post-processing filter in which you can use regular expressions to modify code. After considering other approaches to modifying WordPress and using its own APIs for this, I decided that it would be simplest to simply replace absolute links to FleeTheJungle.com with relative addresses. This allowed users visiting a city-based sub-domain to remain within the geolocal site:
Prior to configuring this, visits to Portland Flee the Jungle sub-domain had outbound links to the global fleethejungle.com scattered across pages. After configuring it, outbound links remained consistent with the inbound domain or sub-domain.
Essentially, I used the post-processing filter to eliminate absolute URLs and allow the browser’s URL and relative addresses to drive navigation.
City Listings
To provide a directory of the cities where Flee the Jungle is and would become available, I built a page listing cities where global users could jump into. I also use it to attract volunteers to launch sites for their cities.
The Completed Configure_Geolocal() Function
Here’s the final code we used within WordPress for configure_geolocal():
function configure_geolocal() global $nonlocal_category_ids; global $known_cities; global $domain_locale; global $logo_image_url; // Tag IDs for city names e.g. Seattle, Portland $known_cities = array(49,211); // Category IDs that are worldwide, digital to be displayed without changes // e.g. Getting Started, Prime and Smile Alternatives, Streaming Media, etc. $nonlocal_category_ids = [1,22,29,30,46,57,60,195]; $domain_locale = arrested_subdomain(); if ($domain_locale == 'www' or $domain_locale == 'fleethejungle') $domain_locale = false; $logo_image_url = '/wp-content/uploads/2015/10/ftj-logo-global.gif'; else if ($domain_locale == 'pdx' or $domain_locale =='portland') $domain_locale = 'portland'; $logo_image_url = '/wp-content/uploads/2015/10/ftj-logo-portland.gif'; else if ($domain_locale == 'sea' or $domain_locale =='seattle') $domain_locale = 'seattle'; $logo_image_url = '/wp-content/uploads/2015/10/ftj-logo-seattle.gif'; else // unknown city - redirect wp_redirect( 'http://fleethejungle.com/cities' ); exit;
Miscellaneous Requirements
Customizing the Logo
Of course, I wanted to implement basic customization of the logo depending on the sub-domain. The KnowHow theme doesn’t support this natively.
If you notice in the final configure_geolocal()
code, I set a media library URL for the logos for each city.
Then, the code within header.php customizes the theme logo URL based on the domain here:
<!-- #logo --> <div id="logo"> <?php if (is_front_page()) ?><h1><?php ?> <a title="<?php bloginfo( 'name' ); ?>" href="<?php echo home_url(); ?>"> <?php if (of_get_option('st_logo')) ?> <img alt="<?php bloginfo( 'name' ); ?>" src="http://code.tutsplus.com/<?php echo $GLOBALS["logo_image_url"] ?>"> <?php else ?> <?php bloginfo( 'name' ); ?> <?php ?> </a> <?php if (is_front_page()) ?></h1><?php ?> </div> <!-- /#logo -->
Hosting Domains and Sub-Domains
WP Engine’s domain mapping can be a bit confusing for the uninitiated. You have to add domains for your site and map them to WP Engine’s internal addressing system. If you want sub-domains to operate properly, you have to individually configure each sub-domain from within the WP Engine domains dashboard.
This took me a little while to figure out. Here I am adding a placeholder arrival page for San Francisco, which can be reached by sanfrancisco.fleethejungle.com or sfbay.fleethejungle.com. WP Engine makes it easy to set up these redirects:
Here’s a preliminary view of domains configured for global and local Flee the Jungle:
Unfortunately, cities that you don’t explicitly add redirect to a 404 error at WP Engine. There are other ways to work around this, but for the moment, I’m just adding larger cities to the configuration there. People will generally be directed to the global home page or a pre-existing city—from there, they can volunteer to start their own city.
Other Issues
Google’s internal search engine optimization is well known for being quite secret, yet much of the referral traffic your site receives depends on it.
Some of our content will appear across multiple domains, but much of it will be distinct depending on which city we’re browsing. Google doesn’t like seeing the same content on multiple sites, but it’s also good at indexing sub-domains separately.
Frankly, I have no idea how the Flee the Jungle geolocal network model will be received and ranked within its great supercomputing megaplex. This is a lower priority for me at the moment. The foundation of Flee the Jungle’s traffic is grassroots support and social sharing.
In Closing
Please compare the differences in content between worldwide Flee the Jungle and Portland’s local city site:
Certainly, there are a number of ways I could have approached the challenge of creating geolocal sites. However, Flee the Jungle remains a volunteer effort—I needed a solution that would be simple and fast while making it easy to add cities, leveraging bloggers as volunteers. The approach I’ve described has accomplished these goals quickly.
The result of all this customization is that we have a single WordPress installation that can run hundreds of geo-local variations of our site, with worldwide and local content appropriately filtered and organized. Most importantly, volunteers can build and launch cities with only the basic skills of a WordPress blogger. This is so much simpler and faster than my “friend” WordPress multisite or running separate installations. The future is relatively unbounded.
I hope you’ve enjoyed this tutorial and would enjoy hearing how you might have approached this challenge. If you’d like to launch a shopping guide for your own city, contact us. If you’re intrigued about building a site like this, I’ve written a detailed, step by step tutorial for building your own knowledge base website on WordPress. Let’s say you want to launch your own Harry Potter fan site—it’s an easy guide to getting started.