Making P&T: Structure and URLs
Posted by Brandon Kelly on Mar 3, 2010
This is the second installment in a series about the design and development behind what is now Pixel & Tonic. Yesterday we looked at the origin of the name and logo, and a bit of the design process. Today I’m going to focus on how the site is structured.
One of the great, horrifying things about ExpressionEngine is that your website’s structure is completely up to you. EE makes no assumptions whatsoever, which makes it a fantastic tool for everything from simple blogs to full-blown portals like iLounge. And the best part is that, should you decide to scale your website into something bigger, you can do it without starting from scratch. Your content is already sitting in non-assuming weblogs / channels / sections / buckets, ready to be presented in any variety of new ways.
This was essentially the story with my previous site. It began as a simple Movable Type-based blog, whose content was a mix of blog posts, portfolio entries, and a couple downloadable goods. Once I started releasing ExpressionEngine add-ons, I attempted to restructure things a bit by applying categories to entries. But it didn’t take long before the site started showing its seams, so in December of 2008 I decided to do the right thing, and migrate to EE.
The migration was simple enough – I used the Movable Type Import Utility to get the content into EE, and then I distributed my entries into separate weblogs: Sites, EE Add-ons, Mint Pepper, Mac Apps, and Posts. I came up with a basic design that only consisted of a homepage and a single entry page, and called it a day. Since then, I’ve revamped the homepage to focus solely on EE add-ons (only giving other content a small home in the global footer), revamped the blog, introduced documentation sections for FieldFrame and Playa, created the Playa in Action page and Fieldtype Showcase, and made a few other iterative enhancements.
URL Routing
While that site is a testament to ExpressionEngine’s malleability, I’ve never been very happy with its structural implementation. A prime example of that is how I’m handling URL routing. I wanted all product URLs to begin at the first URL segment, e.g., http://brandon-kelly.com/playa, rather than http://brandon-kelly.com/ee/playa. I accomplished that using segment conditionals:
{if segment_1 == ""}
{embed="site/_home"}
{if:else}
{embed="site/_apps"}
{/if}
In this case, if the site/index template gets called (and thus, the request hasn’t already been routed to an existing template group, like “blog”), I’m assuming that either the homepage or a product of some sort should be displayed.
While this approach works, it’s not ideal, since things like template caching go out the window, not to mention the performance hit from loading and parsing an additional template. So when I set out to make pixelandtonic.com, I decided to try something new.
I started with a simple and straightforward template structure. The “site” group is for the homepage and globally-embedded templates, like the site header and footer. Then there’s an “ee” group for ExpressionEngine add-ons, which includes the templates “index”, “releasenotes”, and “docs”. The ee/index template checks {segment_2} for the add-on’s URL Title, and the other two check {segment_3}.
When you visit an EE add-on’s section on pixelandtonic.com, the first step in URL routing takes place not in a template, but in the site’s .htaccess file:
RewriteRule ^index.php/(playa|wygwam|fieldframe|ffmatrix)(/[^\/]+)?(/.+)?$ /index.php/ee$2/$1/$3 [NC,L]
This little redirect (coupled with a standard index.php redirect) will take an incoming URI like /playa, and turn it into /index.php/ee/playa behind the scenes. Sub-pages like /playa/releasenotes become /index.php/ee/releasenotes/playa. Now, the first template EE loads is also the last (well, besides the site header and footer embeds). No template segment conditionals required!
My only caveat is that it will require manually adding new URL Titles to the regular expression as I introduce new products, but that’s certainly an acceptable tradeoff.
Trailing Slashes
While on the subject of .htaccess redirects, there’s another little tidbit probably worth sharing: how I’m handling trailing slashes in the URI. While EE doesn’t know the difference between URIs like /playa versus /playa/, web analytics apps and search engines may consider these separate web pages. If you’re developing a static website, this isn’t a big deal, because if you attempt to go to the former URI (sans trailing slash), Apache will automatically redirect the client to the latter (with trailing slash).
But for a web application like EE, where everything in the URI after “index.php” is handled by the application rather than Apache, this redirection is left up to you. Just like the decision to use or not use a “www” subdomain, it doesn’t matter whether you choose to force a trailing slash or vise-versa; it just matters that you force one or the other.
I decided to force URIs without trailing slashes, if only because they look a little cleaner, and in this 140-char world, every character counts. Here’s the rewrite rule I’m using to do it:
RewriteCond %{REQUEST_METHOD} !=POST
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]
If you decide to force URIs with trailing slashes instead, simply change that last line to:
RewriteRule ^(.+[^/])$ /$1/ [R=301,L]
For this redirect to be applied, it must first pass a couple conditions that make sure that this isn’t a POST request, and the URI does not map to an existing file or directory. (They’re the same conditions I’m using for index.php redirection.) The rule must be placed before your index.php rule, since by that point, your request has already been redirected to an existing file (index.php).
SSL
Being that I’m handling payments now, SSL was crucial. But I only wanted it on certain pages (anything in members/, and store/checkout). So after getting my SSL certificate up and running, I set a new redirect in place to force SSL on these pages:
RewriteCond %{ENV:SECURE_REDIRECT} !=on
RewriteRule ^(members|store/checkout) https://%{HTTP_HOST}%{REQUEST_URI} [NC,R=301,L]
Note that %{ENV:SECURE_REDIRECT} is a custom environment variable set by EngineHosting. Due to the way their servers are set up, the normal %{HTTPS} variable doesn’t reflect the actual SSL status of the request. If you’re not on EngineHosting, use %{HTTPS} instead.
Then there was the problem of forcing forms living on SSL pages to also post to SSL pages. (Since most forms on P&T are generated from EE modules which just use the Site URL as the form action’s prefix, this wasn’t as simple as remembering to type “https://”.) For that I wrote a simple plugin that converts all instances of “http://” into “https://”:
function https()
{
$tagdata = $this->EE->TMPL->tagdata;
return str_replace('http://', 'https://', $tagdata);
}
Wrapping module form tags with {exp:pt:https} and {/exp:pt:https} then did the trick.
So that covers the initial HTML response, but not the in-page elements (CSS, JS, images). And unfortunately, some browsers make a big fuss when HTML is transferred over SSL, but not the in-page elements.
To force all elements to load over the same protocol the HTML was being sent in, I opened up index.php, and added a couple new global variables:
$assign_to_config['site_url'] = 'http://'.$_SERVER['HTTP_HOST'].'/';
$assign_to_config['global_vars']['site_ssl_url'] = str_replace('http://', 'https://', $assign_to_config['site_url']);
$assign_to_config['global_vars']['site_proto_url'] = ($_SERVER['HTTPS'] == 'on' ? $assign_to_config['global_vars']['site_ssl_url'] : $assign_to_config['site_url']);
Now, {site_url} is being defined right in index.php, {site_ssl_url} will always be the same as {site_url} except with an https:// prefix, and {site_proto_url} is either set to the {site_url} or {site_ssl_url}, depending on whether or not the current request is coming over SSL.
From there, it was just a matter of plugging these new tags in where appropriate. Links to members/* (which would be redirected to https:// anyway) are now created using {site_ssl_url}:
<li><a id="sitenav-signin" href="{site_ssl_url}members/signin">Sign In</a></li>
<li><a id="sitenav-register" href="{site_ssl_url}members/register">Register</a></li>
And page resources are pulled in using {site_proto_url}:
<link rel="stylesheet" type="text/css" href="{site_proto_url}global/styles/pt.css" />
The end result is that pages which don’t require SSL don’t get SSL; links going to pages that should be served with SSL have appropriate href attributes; pages that should be on SSL are forced to be served over SSL as well as their resources, and forms living on those pages will submit over SSL. All bases covered.
And that’s it for today; thanks for tuning in! This series will continue tomorrow with a look into how I’m handling product documentation, so make sure you’re following @pixelandtonic or subscribed to our RSS feed to be notified about that.
Comments
Adam Wiggall
Brandon,
Great write up, thanks for sharing in such detail. I know that this will be a valuable resource for me, and I’m sure many others.
Great work.
Adam
Olivier
Brilliant, thank you for sharing. I’m sure this will be very useful very soon
Buggles
Superb write up
Peter
Nice writeup, thanks for the insights Brandon!
Your site turned out great by the way.
Jeff Pullinger
You’re a very generous genius
thanks for sharing your process. You’ve (again) saved mr a lot of time.
Andrew Gunstone
Great writeup. Agree with all the other comments.
Quick question though, couldn’t you have used the built-in CodeIgniter routing? Obviously you won’t be able to solve all your issues with that (i.e. not the SSL issues), but I thought it might have been a cleaner solution.
That said… if it ain’t broke!
Andrew Gunstone
My previous comment was actually assuming that you are using EE 2.0 thought!! I should have mentioned that.
Danny Tam
Thanks for the insight. SSL is certainly something I’ve never touched into too much.
Regarding URL routing, is there any good solutions from changing site/weblog/title into site/title for sites that are content heavy with thousands of posts? Can this be done with something like LG .htaccess generator? What solutions for EE 2.0?
Thanks again for the great writeup!
Brandon Kelly
@Andrew – I am using EE2, but to be honest I didn’t look into CI URI routing. Perhaps I’ll spend some time investigating that and do a follow-up post.
@Danny – If the content is going to change more often than the template groups, you could instead make the redirect apply to anything that doesn’t match your template groups:
RewriteCond %{REQUEST_URI} !^/index.php/(ee|blog|contact|store|members)RewriteRule ^index.php/(\w+)(/\w+)?(/.+)?$ /index.php/ee$2/$1/$3 [NC,L]
Jim
This is great stuff! Thank you so much for taking the time to write this up.
I’m actually using the conditional approach on a new build, mainly because I heard about it on Dan Benjamins ee podcast and wanted to try it out.
Is there really that much of a speed difference between that approach and the .htaccess you went with?
Danny Tam
@Brandon — Thanks for the quick tip. I’ll definitely put it to good use. I have to study on the syntax. Asides from the Apache Tutorial, any good reads on this subject?
Brandon Kelly
@Jim – I don’t have any stats, but spend a couple days hanging out with Paul Burdick and your new life mission will be to avoid extra
{embed}s at all cost.@Danny – Biggest thing is just learning Regular Expressions. Once you’re comfortable with those, there’s nothing to mod_rewrite redirects.
Andrew Gunstone
I just wanted to follow up my comments from earlier with the documentation for URL routing in CodeIgniter. Whilst I haven’t tested it, I am assuming that it will work okay in EE2 (which as we all know is built on CI).
You can check out the docs for CI Routing here - http://codeigniter.com/user_guide/general/routing.html
Jim
I had another question, regarding the trailing slash debate. When I was heavy researching this last year I got the impression that things would run faster if there was a trailing slash because the server wouldn’t think that last segment was a file and spend time looking for it.
Is this just some rumor or am I right in thinking that?
Brandon Kelly
That only applies to Apache – not going to affect EE’s performance. (In fact, EE2’s {path=} tags now omit the trailing slash.)
Luke Dorny
Wonderful writeup, Brandon!
Thanks for taking the time.
Luke Dorny
btw, i clicked on your name in comments and it went to https and borked.
delete this at will.
Luke Dorny
The trolls have arrived.
btw, i get spam comment emails on a TON of ee sites. A solution for this needs to be addressed.
danielle r.
The SSL info is exactly what I have been looking for!
I was able to do the redirect but had to toy w/ it a bit to get it work. Here is what worked for my setup that has MSM installed:
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(index.php/members|site2/index.php/members|syscore) https://%{HTTP_HOST}%{REQUEST_URI} [NC,R=301,L]
danielle r.
I’m at a bit of a loss when it comes to the second bit (creating the plugin) - I realize this info is probably for more advanced users who can fill in the blanks.
Is there any hints or more specific info on how to set this up? I can’t sort out plugin “how to” in relation to the what was written, what is the “pt” etc.
I created a plugin called pi.https.php with a class called Https and put the function https() inside.
I know that’s wrong because if I try to wrap the form with{exp:pt:https} & {/exp:pt:https} I get the error “The following tag has a syntax error: {exp:pt:https}” and if I try to remove the “pt” I get “Notice: Trying to get property of non-object…”
AND for the third part on adding the global vars just seems to get ignored - {site_ssl_url} isn’t being identified as a variable.
ANY help would be appreciated!
rebecca
brandon! just what i was doing on a client site! you are generous with your clever solutions. i’m on EH but the .htaccess above isn’t working for me? any thoughts on what i have wrong in this?
RewriteEngine on
RewriteCond $1 !^(images|basys|themes|js|css|docs|flash|photos|email|uploads|favicon\.ico|robots\.txt|index\.php|sitemap\.php) [NC]
RewriteRule ^(.*)$ /index.php?/$1 [L]
RewriteCond %{ENV:SECURE_REDIRECT} !=on
RewriteRule ^(contact|team/app) https://%{HTTP_HOST}%{REQUEST_URI} [NC,R=301,L]
Lea
I just want to pipe in how great this is and I’m implementing the way you’ve handled URL segments on a client site (muuuuch cleaner).
The only thing I want to add is for anyone implementing this with segment conditionals with this technique, is to add an extra segment to what you’re dealing with. Even though you’re hiding the template group, EE still recognizes it as “segment_1” so your first segment with this technique is now actually segment_2. Ran into this when I was using Low Seg2Cat in conjunction.
Cheerios!
Ryan B
Hey Brandon,
Quick question, hopefully you don’t mind me asking. I was wondering if the code you referenced in both areas about the simple plugin you created and the new global SSL variables you created in index.php are code specifically for EE 2.0? I am trying to achieve something similar on a site I am building and can’t seem to get either approaches working.
Lemme know if you have a sec, thanks (: