Blog / How To

Simple ExpressionEngine Stash Parse Order

Written in Amherst, Massachusetts on January 9 by Noah Kuhn

Stash is a great addition to the ExpressionEngine toolkit, but sometimes the parse order can get a little confusing. One great thing about Stash is the ability to set variables at the top of a template that can be used later on in your code. This allows you to keep a nice separation between the data and the template itself. I'm not going to be talking about template partials at all as there are a bunch of great articles about that floating around. This post is just outlining a specific problem and it's solution.

If you need to use a Stash variable that was created within a Channel Entries loop earlier in a template as a parameter in a Channel Entries loop, you can delay the parse order of the second Channel Entries loop such that the variable gets set prior to the loop being executed. For example if you got a list of entry IDs of related entries (from Relationships or Playa) you could put those in the entry_id parameter and then print out the entries in the correct order. This could be used for categories, statuses, search values, whatever.

There are always a bunch of ways to do something in EE but this method has proven handy in several recent builds to get around needing embeds or other more complex nesting.

{exp:stash:parse process="end"}
  {exp:channel:entries channel="XXX" fixed_order="{stash:places}" entry_id="{stash:places}" dynamic="no"}
    {title}
  {/exp:channel:entries}
{/exp:stash:parse}

ExpressionEngine Pagination with Structure and Zoo Triggers

Written in Northampton, Massachusetts on June 15 by Noah Kuhn

Using Structure with ExpressionEngine is one of the most client-friendly things you can do for your EE sites. It allows you to set up a nice tree view of your site content, complete with drag & drop, page additions, and a slew of nice display functions for the front end.

One big sticking point when using Structure is that it overrides your default EE URL system, which can be troublesome when you want to get things like categories and date based archives working within Structure.

Say you want to set up a WordPress-style date/category area of your site, like a blog or a news area. When I first got this working within Structure, it required a ton of ugly PHP to check segment values and display different content based on the results. Not very elegant and inefficient. I knew there had to be a better way.

After looking around for some solutions, I stumbled across Zoo Triggers. From the developer:

No more fiddling around and wasting time trying to get categories and archives to play nice with your Structure setup. Zoo Triggers is the add-on that fills the gap between Structure and 'categories and archives'. Within seconds you have 'category, archive and tags' filters on your base Structure install.

You can read more about the Zoo Triggers setup on their site, but it is dead simple. You just add a simple tag to your channel entries loop and it will automatically update your entries based on the URL. They even have handy Category and Date based tags to output your sub navigations. These come with a bunch of options to tailor your sub navigation to your site. The documentation could use some updating, as a bunch of options are not currently described. I'm sure this will be updated soon.

Using Zoo Triggers with Structure works remarkably well. Your entries are all controlled through Structure (as a listing beneath a main "blog" page). All you need are two templates. One template (blog_list) for both the main page and any date & category listing pages. This one page simply gets updated based on the URL structure to show the appropriate entries. Then a second template for the single entry page. You could probably do it all in one, but I like the separation. Set up your listing in Structure and then set the default template for the listing entries to be the single entry template (blog_single). Then just insert the navigation tags you want to use and you are pretty much done.

NOTE: Make sure you do NOT have Dynamic Channel Caching on. If you do, the pagination will not work. I'm not sure if this is a by product of Structure, Zoo Triggers, or just a problem with EE, but it tripped me up for a while. Basically the query for the first page gets cached and when you visit any other "page" in the section, it just shows the original entries that were cached. You can turn it off in Admin > Channel Administration > Global Preferences.

ExpressionEngine config.php

Written in Northampton, Massachusetts on May 18 by Noah Kuhn

One of the biggest takeaways from the EECI 2011 Conference in Brooklyn for me was the use of a config.php file in ExpressionEngine builds. Matt Weinberg from the Vector Media Group gave a great presentation titled 10 Ways to Rock ExpressionEngine. Using a config.php file allows you to control the numerous configuration options in ExpressionEngine from a single text file. This allows for quick and easy updates, instant provisioning of a new site install, and easy movement from server to server.

I spent some time with different config.php approaches and have settled on a hybrid of some existing versions:

There are several other great resources out there for EE2 configuration:

I'm going to outline the different items that Pilotmade uses in our EE projects. We're always tweaking this, so this approach is where we are in May of 2012.

The idea is to create a file titled config.php and place it at the root level of your site. Within that file you enter the following sections of configuration. Once the file is setup, it has to be included (required) from both /system/expressionengine/config/config.php and /system/expressionengine/config/database.php using the following code. Just place this at the bottom of each file.

require(realpath(dirname(__FILE__) . '/../../../config.php'));

Note: when you go to update your ExpressionEngine install, you have to comment out this code or your upgrade will go into an endless loop. Your standard config and database files MUST be valid and have the correct database credentials for the server you are upgrading on. 

1. Dynamic paths

The first thing to do is to set some dynamic file system paths based on the server the site is located on. This allows us to automatically set things like URL folder paths later on.

$protocol                         = (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https://" : "http://";
$base_url                         = $protocol . $_SERVER['SERVER_NAME'];
$base_path                        = $_SERVER['DOCUMENT_ROOT'];
$system_folder                    = "system";
$images_folder                    = "images";
$images_path                      = $base_path . "/" . $images_folder;
$images_url                       = $base_url . "/" . $images_folder;
$user_agent                       = $_SERVER['HTTP_USER_AGENT'];

2. Environmental Variables (database)

This allows you to enter multiple database credentials based on the server you are on. This makes it very easy to move from one server to another when you launch a new site. This method uses the IP address of the server to determine which credentials to use. Depending on your setup, you may need to use a different method to check the server (hostname etc.), especially if your development and production installs are on the same server.

switch ( $_SERVER['SERVER_ADDR'] ) {

/* Local DB Config */ 
case '127.0.0.1' : 
$db['expressionengine']['hostname'] = "localhost";
$db['expressionengine']['username'] = "username";
$db['expressionengine']['password'] = "password";
$db['expressionengine']['database'] = "database";
$env_global_vars = array(
    'global:env' => 'local'
);
break;

/* Development DB Config */
case '111.222.333.444' :
$db['expressionengine']['hostname'] = "localhost";
$db['expressionengine']['username'] = "username";
$db['expressionengine']['password'] = "password";
$db['expressionengine']['database'] = "database";
$env_global_vars = array(
    'global:env' => 'dev'
);
break; 

/* Production DB Config */
case '444.333.222.111' : 
$db['expressionengine']['hostname'] = "localhost";
$db['expressionengine']['username'] = "username";
$db['expressionengine']['password'] = "password";
$db['expressionengine']['database'] = "database";
$env_global_vars = array(
    'global:env' => 'prod'
);
break;

}

3. Debugging and Performance

Quick access to debugging controls. This is always helpful if something goes awry and your site stops showing up.

$config['show_profiler']                = 'n'; # y/n
$config['template_debugging']           = 'n'; # y/n
$config['debug']                        = '1'; # 0: no PHP/SQL errors shown. 1: Errors shown to Super Admins. 2: Errors shown to everyone.
$config['disable_all_tracking']         = 'y'; # y/n
$config['enable_sql_caching']           = 'n'; # Cache Dynamic Channel Queries?
$config['email_debug']                  = 'n'; # y/n
$config['autosave_interval_seconds']    = "0"; # Disabling entry autosave

4. General Paths

Standard site paths.

$config['index_page']             = "";
$config['base_url']               = $base_url . "/";
$config['site_url']               = $config['base_url'];
$config['cp_url']                 = $config['base_url'] . $system_folder . "/index.php";
$config['theme_folder_path']      = $base_path . "/themes/";
$config['theme_folder_url']       = $base_url . "/themes/";

5. Universal Database Settings

These are mostly the values from the standard database.php file.

$db['expressionengine']['dbdriver']  = "mysql";
$db['expressionengine']['dbprefix']  = "exp_";
$db['expressionengine']['pconnect']  = FALSE;
$db['expressionengine']['swap_pre']  = "exp_";
$db['expressionengine']['db_debug']  = FALSE;
$db['expressionengine']['cache_on']  = TRUE;
$db['expressionengine']['autoinit']  = FALSE;
$db['expressionengine']['char_set']  = "utf8";
$db['expressionengine']['dbcollat']  = "utf8_general_ci";
$db['expressionengine']['cachedir']  = $base_path . $system_folder ."/expressionengine/cache/db_cache/";

6. Template Preferences

This lets you set up the defaults for how templates are treated. I save templates as flat files so I have that set to yes. I also move the templates folder out of /system/ into the root level of the site. Just make sure you have an htaccess file set to deny from all so your templates folder isn't open to the world.

$config['save_tmpl_files']            = "y";
$config['site_404']                   = "site/404";
$config['strict_urls']                = "y";
$config['tmpl_file_basepath']         = $base_path . "/templates/";
$config['hidden_template_indicator']  = ".";

7. Member Directory paths and URLs

Moving from server to server means paths get will likely get updated. Having these dynamic is a big help.

$config['emoticon_path']        = $images_url . "/smileys/";
$config['captcha_path']         = $images_path . "/captchas/";
$config['captcha_url']          = $images_url . "/captchas/";
$config['avatar_path']          = $images_path . "/avatars/";
$config['avatar_url']           = $images_url . "/avatars/";
$config['photo_path']           = $images_path . "/member_photos/";
$config['photo_url']            = $images_url . "/member_photos/";
$config['sig_img_path']         = $images_path . "/signature_attachments/";
$config['sig_img_url']          = $images_url . "/signature_attachments/";
$config['prv_msg_upload_path']  = $images_path . "/pm_attachments/";

8. Global Variables

This section allows you to set up some really handy global variables to use throughout your site. You can easily add to this to create nice shortcuts during development.

$default_global_vars = array(

	// Tag parameters - Short hand tag params
	'global:disable_default'   => 'disable="categories|pagination|member_data"',
	'global:disable_all'       => 'disable="categories|custom_fields|member_data|pagination"',
	'global:cache'        => 'cache="yes" refresh="10"',
	'-global:cache'       => '-cache="yes" refresh="10"', // disable by adding a '-' to the front of the global

	// Date and time - Short hand date and time
	'global:date_time'          => '%g:%i %a',
	'global:date_short'         => '%F %d, %Y',
	'global:date_full'          => '%F %d %Y, %g:%i %a',

	'global:isipad'    => (bool) strpos($user_agent,'iPad'),
	'global:isiphone'   => (bool) strpos($user_agent,'iPhone')

);

// Make this global so we can add some of the config variables here

global $assign_to_config;

if(!isset($assign_to_config['global_vars'])) {
	$assign_to_config['global_vars'] = array();
}

$assign_to_config['global_vars'] = array_merge($assign_to_config['global_vars'], $default_global_vars, $env_global_vars);

That's all I have for now. Using a config.php file can speed up initial site installs, simplify server moves, and add convenient ExpressionEngine configuration options. Definitely worth the effort.

Apache Query String URL to SEO Friendly URL

Written in Northampton, MA on November 10 by Noah Kuhn

On one of my current migration projects. I need to rewrite a bunch of clunky old dynamic query string URLs to nice clean search engine friend URLs. Took a bit of digging through Apache documentation, but here it is.

The goal was to go from

http://domain.tld/?tag=somekeyword&Template=feed&IncludeBlogs=71

to

http://newdomain.tld/feeds/topic/somekeyword/

If you want to rewrite just one URL, simply type it in as written below. The downside to this approach is that, it's just one URL. You would have to retype this for each string you wanted to rewrite.

RewriteEngine On
RewriteCond %{QUERY_STRING} ^tag=blah&Template=feed&IncludeBlogs=71$
RewriteRule ^index.php$ /feeds/topic/blah/? [L,R=301]

Or if you want to get fancy and some piece of the original URL actually matches the new URL, You can store it and then :

RewriteEngine On
RewriteCond %{QUERY_STRING} ^tag=([a-z]+)&Template=feed&IncludeBlogs=71$
RewriteRule ^index.php$ /feeds/topic/%1/? [L,R=301]

In both examples, index.php is an example and would be whatever your script address is.

Gmail + iPhone + Apple Mail

Written in Concord, MA on November 8 by Noah Kuhn

For a while now I’ve been using several different Gmail/Google Apps accounts with Apple Mail and the iPhone. There are an abundance of guides out there describing the best way to integrate everything so you have the same user experience in webmail, desktop, and mobile. I’ve tried a bunch of these but they all seem to fall short of doing things how they should. I wanted the experience to be as close to a normal IMAP server as possible, where the mindset is that emails should be deleted, NOT archived. This post comes to the same conclusions, albeit with some screenshots and some elaboration.

A few necessary aspects of my “perfect” setup.

  1. Delete a message on any platform, it gets put in the trash, NOT archived.
  2. Move a message to a local folder in Apple Mail, it gets put in the trash on the server, NOT archived.
  3. Get rid of All Mail so duplicate messages aren’t downloaded and the badge count is correct.
  4. Clean up the folder hierarchy in Apple Mail such that there isn’t a [Gmail] folder and all Gmail folders are correctly mapped (Spam, Sent, Trash etc.).
  5. Nested Folders.

Gmail / Google Apps Setup

First, we need to get your Gmail / Google Apps account set up correctly with IMAP to allow us to access things from Apple Mail and the iPhone. This involves enabling two features from the Labs section, customizing the IMAP behaviors, and customizing which IMAP folders display in Apple Mail. Follow the instructions below.

  1. Settings > Forwarding and POP/IMAP > IMAP Access > Status > Enable IMAP
  2. Settings > Forwarding and POP/IMAP > IMAP Access > When I mark a message in IMAP as deleted: > Auto-Expunge off
  3. Settings > Forwarding and POP/IMAP > IMAP Access > When a message is marked as deleted and expunged from the last visible IMAP folder: > Move the message to the Trash
  4. Click “Save Changes.” Then go to:
  5. Settings > Labels > System Labels > Starred > Uncheck “Show in IMAP”
  6. Settings > Labels > System Labels > Chats > Uncheck “Show in IMAP”
  7. Settings > Labels > System Labels > All Mail > Uncheck “Show in IMAP”

You’re done, now open Apple Mail.

Apple Mail Version 6.2 (1499) Setup

My goal was to map all the Gmail folders to the appropriate Apple Mail folders so things like Drafts, Sent Mail, Trash and Junk were available on the left mailboxes view in the main sections, NOT within the account folders below. This setup differs greatly from Google’s suggestions but it has been working well for me. This method will leave the [Gmail] folder in Apple Mail, but it will be empty. If you actually want to remove that, you have to get a bit more complicated. I plan to outline that method in another post.

  1. Create a new account. You can figure out how to do this on the Gmail Help page.
  2. Go to: Mail > Preferences > Accounts > The account you added > Mailbox Behaviors > Make sure everything is checked.
  3. Go to: Mail > Preferences > Junk Mail > Enable Junk Mail filtering and select “Move it to the Junk mailbox” option. Close the Preferences window.
  4. In the left hand Mail folder pane, find your account name and expand the [Gmail] folder
  5. Map the folders within to their correct functions in Apple Mail by selecting the appropriate folder and then going to: Mailbox > Use This Mailbox For:
    • Drafts > Drafts
    • Sent Mail > Sent
    • Spam > Junk
    • Trash > Trash

Apple iOS 6.1 Setup

Now on the iPhone we want to have the same clean folder hierarchy and have deleted messages actually get put in the trash instead of archived.

  1. Set up a new email account: Settings > Mail, Contacts, Calendars > Add Account… > Gmail > Enter your info
  2. Settings > Mail, Contacts, Calendars > Select your account > Archive Messages > OFF
  3. Settings > Mail, Contacts, Calendars > Select your account > Account > Advanced > Mailbox Behaviors
    • Drafts Mailbox > Set it as the Gmail Drafts folder
    • Deleted Mailbox > Set it as the Gmail Trash folder

What did all this do?

We’ve successfully mapped all folders correctly in Apple Mail and on the iPhone. Deleting works as it should, actually moving the email into the Trash instead of just Archiving it.