Chapter 27: 404 Management
EE has some ways of helping you control what happens if an invalid URL is submitted. There are global preferences and template-level code that we can use. After some initial setup work we'll go through our site section by section and get 404 rules in place.
Create a 404 Template
Before we add our rules let's create a specific template that will load under 404 conditions.
Create a new template in your site Template Group and name it 404. Here is the code for it - also found in the Companion Files:
Template Name: site/404
{embed="embeds/html_header" my_page_title="Page Not Found"} {embed="embeds/page_header" the_url_title="page-notfound-intro" my_location=""} <div id="maincontent"> <div id="right_side"> {snp_latest_news} {snp_latest_products} </div> <div id="left_side"> {exp:channel:entries channel="miscellaneous content" disable="categories| member_data|pagination" url_title="page-not-found"} <h2 class="underline">{title}</h2> {page_body} {/exp:channel:entries} </div> </div> {snp_footer}
Companion Files: chapter_27/site/404.txt
The only real thing to point out about this code is that it assumes two new entries have been created in the Miscellaneous Content Channel with url_titles of pagenot-found and page-not-found-intro. This approach ensures that your clients will be able to change this messaging if needed in the future. Take a moment to publish those entries now.
Configuring Global Preferences
We need to tell ExpressionEngine to load our new template when 404 conditions are met. This setting is found under:
Design > Template Manager > Global Template Preferences > 404 Page.
Choose the site/404 template you just created. But, don't save just yet as there is one more setting I want to look at here.
Figure 112: Setting the 404 Template
Validating the First URL Segment - Strict URLS
Our first pass at 404 management is a pretty easy one that will both validate the first segment of all URLs and set the stage for deeper 404 management.
ExpressionEngine will let you omit the Template Group name from links to templates contained in the Template Group that holds your site index template (the "site" group in our case).
In other words, if your site's home page template is in a Template Group named "site", and that group also contains a template called "site_map", either of the following two links would load that template:
http://mysite.com/index.php/si...
http://mysite.com/index.php/si...
This behavior also allows for invalid first-segment links to load the Home page. On our test site, the following URL would resolve to the site Home page even though there is no template or Template Group named "mike":
http://yourtestsite.com/index....
The way to force EE to have a more deliberate and valid first-segment template loading approach is to use Strict URLs. Using strict URLs ensures that the Template Group name is present and valid in the first segment position.
The option to turn these on or off is also in Global Template Preferences (where we just set the 404 template). If you moved away from that page, here's the path back to it:
Design > Template Manager > Global Template Preferences > Enable Strict URLs.
Make sure that's set to Yes (it should be Yes by default). Just for reference the EE User Guide on this feature can be found here:
http://ellislab.com/expression...
With that in place, if you manually append an invalid first-segment value onto the URL, EE should load your 404 template:
Figure 113: 404 Result for Invalid Template Group
Home and About Index Templates
With our first segment values now being validated, we can start to move through the rest of the site and build 404 rules in each section. The exact approach will depend on the depth of content and if we have specific templates for multi-entry and single-entry modes.
Let's tackle our Home and About pages as they function a bit differently than other areas of our site. Why? Links from search results - that's why.
Think back to Chapter 24 where we configured the Search Results URLs. Remember that we configured the Search Result URLs for the Home and About Channels to point at these index templates. If a user's search now contains a hit on one of these pages, the EE search engine will build a URL with the url_title of the entry appended to the segment_2 position. For example, if a search includes a hit from the Our Assets post in the About Channel, the search result link would be:
http://yoursite.com/index.php/...
This is valid URL because we left the {exp:channel:entries} tag pair on that template in dynamic mode. This means, when it sees a valid url_title in the URL it will dynamically self-filter and display just that one entry.
The net result is that these two templates are used in both single-entry and multientry mode, so the 404 management will be a bit more complex.
We'll start with an approach used on single-entry templates (we'll see this again on products/detail, for example). It's done with a combination of a parameter, a Conditional Variable, and a Global Variable:
- Parameter: {require_entry="yes"}
This is a parameter for the {exp:channel:entries} tag pair and tells the tag that the entry_id or entry's url_title must be valid. If this is set to yes and the url_title appearing in the URL is invalid, the tag will return no
results. You can read more about this parameter here:
http://ellislab.com/expression... - Conditional Variable: {if no_results}
This is a conditional that fires when the {exp:channel:entries} tag doesn't return anything. Within it you can specify alternate content or messages, or also a Global Variable.
http://ellislab.com/expression... - Global Variable: {redirect="site/404"}
This is a Global Variable that can be used to redirect the user to the 404 template. More on this variable can be found in the EE User Guide:
http://ellislab.com/expression...
These elements are typically used together in the following fashion:
{exp:channel:entries channel="articles" require_entry="yes"} {if no_results} {redirect="site/404"} {/if} <h3>{title}</h3> {article_content} {/exp:channel:entries}
The challenge with this approach is that the require_entry="yes" parameter only works on single-entry templates. We can still use it, but we need to be a bit more crafty with a conditional that first checks to see if the template is being loaded in single-entry mode.
Here's the about/index with our nested conditionals added for 404 management - either key in the new code or grab the entire updated template from the Companion Files:
Template Name: about/index
{!-- 404 code that plays nice when template is loaded from search result with url_title in segment_2 --} {if segment_2 !=""} {exp:channel:entries channel="about" disable="categories|member_data|pagination" sort="asc" require_entry="yes"} {if no_results} {redirect="site/404"} {/if} {/exp:channel:entries} {/if} {embed="embeds/html_header" my_page_title="About"} {embed="embeds/page_header" the_url_title="about" my_location="about"} <div id="maincontent"> <div id="right_side"> {snp_latest_news} {snp_latest_products} Chapter 27 317 </div> <div id="left_side"> {exp:channel:entries channel="about" disable="categories|member_data|pagination" sort="asc"} <h2 class="underline">{title}</h2> {page_body} {/exp:channel:entries} </div> </div> {snp_footer}
Companion Files: chapter_27/about/index.txt
With this code in place, you should be able to:
- View your about/index template normally in multi-entry mode by clicking on About in the main navigation.
- Run a search for a term that appears on the About page, get a result, and follow the link back and view the content on the about template with no error.
- Change the valid url_title for an invalid url_title in the browser address bar and generate your 404 template.
Figure 114: About Page Loaded from Search Result
Figure 115: About Page with Invalid URL Title Appended
This same approach will work on the site/index template and you can find that updated template code in the Companion Files: chapter_27/site/index.txt
These sections are now set for 404 management - so let's move on.
Products, Services and Contact Index Templates
Try this now - navigate to the Products index page on your test site and add a made-up value after /products/ - something like:
http://mytestsite.com/index.ph...
See what happens? Behind the scenes EE is validating segment one and finding that the products Template Group does exist. Within the products Template Group, however, it's not finding a match for "mike". What EE does then is load the closest match it can find, which is the index template.
Figure 116: No 404 for Invalid Product Page
The products/index, services/index, and contact/index templates should never get loaded in single-entry mode, so the 404 management is a bit easier than our Home and About pages.
All you'll need to do is add the following code to the top of those templates:
{if segment_2 != ""} {redirect="site/404"} {/if}
The logic with this approach is this:
- If the URL is valid through segment_2, it must mean there was another template in this group with the name found in segment_2. This code will never execute in that case because the index template is not loaded.
- If the URL isn't valid through segment_2, a template with the name indicated in the segment_2 position wasn't found, so EE will fall back to loading the index template of the Template Group. This code will then execute, segment_2 will have something in it, this conditional will evaluate to true and the user will be redirected to the specified 404 template.
Let's update our products/index, services/index, and contact/index templates with this code. I'll provide the fully updated code for all of the templates in the Companion Files, but it's really just a matter of putting those three new lines of code at the top of the templates.
Once you have that code in place and the templates updated, try the same link as you did earlier. It should now generate the 404 page rather than the products/index template:
Figure 117: Now Getting 404 for Invalid Product Page
Products and Services List Templates
Our next puzzle will be coming up with 404 rules for the products/list and services/list templates. These templates are designed to only be used in category mode - being linked to from the index template in Products and Services. We're looking to throw 404s in three cases:
- The template being loaded without category information in the URL (like if someone changed the URL to just be http://yoursite.com/index.php/products/list).
- The template being loaded with incorrect category information (either a made-up/hacked value or an out of date link to a category that no longer exists).
- The template being loaded in category mode with extra values specified past the category_url_title.
We can handle the first and third cases with the following code - to be placed in the header of your products/list and services/list templates.
{!-- Make sure this template is only loaded in category mode and with nothing appended after the category url title --} {if (segment_3 != "category") OR (segment_3=="category" AND segment_4=="") OR (segment_5!="")} {redirect="site/404"} {/if}
You can grab the fully updated templates from the Companion Files: chapter_27/products/list.txt and chapter_27/services/list.txt
With that code in place and your templates saved, you should be able to trigger the 404 template for the following invalid URLs:
- http://yoursite.com/index.php/...
- http://yoursite.com/index.php/...category/
- http://yoursite.com/index.php/...category/accessories/puppy/
- http://yoursite.com/index.php/...
- http://yoursite.com/index.php/...category/
- http://yoursite.com/index.php/...category/accessories/puppy/
What about the second case - making sure that the category_url_title in segment four information is correct (actually representing an existing category)?
Unfortunately, there is no reliable way using native EE code (that I could find) to trigger 404 pages for templates loading in category mode with an invalid category_url_title. I've posted this as a feature request for EE 2, but as of this writing it's not implemented.
Products and Services Detail Templates
Let's move on to adding 404 logic to the templates that display the most detailed content on our project site - products/detail and services/detail.
First, before modifying our templates, let's run a test so that we'll know if we have things working correctly.
Navigate to an existing detail page for a product and, in the browser URL bar, edit the URL and change the last segment value to something not valid. For example, I navigated to:
http://localhost/index.php/products/detail/onboard_air
and then edited the URL (knowing I don't have an entry in the Products Channel with a url_title of "puppy") to:
http://localhost/index.php/products/detail/puppy
See what happens? ExpressionEngine loads the most recent entry in the Channel:
Figure 118: Product Detail Page with Invalid URL Showing Content
The other case I want to look for is if somehow an extra segment value is appended onto a valid product detail page - for example:
http://localhost/index.php/products/detail/onboard_air/junk/
Let's fix these cases so EE generates a 404 page instead.
To do this, we can reach back to some coding approaches we've already used. Here's the entire products/detail template - give it a scan and then I'll point out the changes:
Template Name: products/detail
{if segment_4 != ""} {redirect="site/404"} {/if} {embed="embeds/html_header" my_page_title="Product Detail {exp:channel:entries channel="products" disable="member_data| trackbacks|pagination" limit="1"} | {title}{/ exp:channel:entries}"} {embed="embeds/page_header" the_url_title="products" my_location="products"} <div id="maincontent"> <div id="right_side"> {snp_latest_news} {!-- By moving the opening tag into the sidebar HTML we'll avoid having to run it once here and again in the left_side div --} {exp:channel:entries channel="products" disable="member_data|pagination" limit="1" require_entry="yes"} {if no_results} {redirect="site/404"} {/if} {!-- Here we'll return the list of related items --} {product_related channel="products"} {if product_related:count==1} {!--only create the HTML if there are related entries --} <h3>Related Products</ h3> <div class="lcontent"> {/if} {!-- Return each related product --} <p><a href="{product_related:url_title_path='products/detail'}"><img src="{product_related:product_image:sidebar}" width="59" height="44" alt="{product_related:title}" /></a> <strong>{product_related:title}: </strong> {exp:word_limit total="15"} {exp:low_replace find="<p>|</p>" replace="" multiple="yes"} {product_related:product_description}{/exp:low_replace}>{/ exp:word_limit} <a href="{product_related:url_title_path='products/detail'}">more >></a></p> Chapter 27 327 {if product_related:count==product_related:total_results} </div> {/if} {/product_related} </div> <div id="left_side"> {!-- Remember we are still within the main channel:entries loop here --} <h2 class="underline">{title}</h2> <img class="category_image" src="{product_image}" alt="{title}" title="{title}" /> Number: <strong>{product_number}</strong> Colors Available: <strong>{product_color}</strong> In Stock?: <strong>{product_in_stock}</strong> {product_description} {product_variants} {!-- Only create an unordered list if there are variants --} {if product_variants:count==1} <h4>Product Variants</ h4> <ul> {/if} <li> <em>{product_variants:variant_name} offered {product_variants:first_offered format='%m/%d/%y'}</em> {product_variants:variant_description} </li> {!--Close the unordered list after the last one --} {if product_variants:count == product_variants:total_results} </ul> {/if} {/product_variants} {/exp:channel:entries} </div> </div> {snp_footer}
Companion Files: chapter_27/products/detail.txt
Notes:
- I've added a conditional to the top of the template to handle any junk segments being appended on in the 4th segment position after the entry url_title.
- The main {exp:channel:entries} tag pair now has require_entry="yes" as a parameter.
- I've added the {if no_results} conditional and use it to redirect to our 404 template if it's true.
Your services/detail template needs to reflect these changes as well, so edit your products/detail and services/detail templates or grab them from the Companion Files: chapter_27/products/detail.txt and chapter_27/services/detail.txt.
With that code in place, let's try to load that same invalid URL and see what happens:
Figure 119: Invalid Product Detail URL Generating 404
Cool! We've got the detail templates nailed down. Let's move on.
Weblog Templates
Let's start with the weblog/index template look first at the type of URLs that need to work here. This template is used in three ways - index mode, category mode, and archive mode, so it needs to allow URLs like these:
http://localhost/index.php/weblog
http://localhost/index.php/weblog/category/company_news
http://localhost/index.php/weblog/2010/04
Due to how we're using this template, the second and third segments may or may not have values in them and they can be either category content or archive dates. This makes it pretty tough to do any in-depth 404 handling. The only thing I can see doing here is to ensure that anything past the third segment doesn't get junk values placed in it. I'll just go with this code at the top of the weblog/index template:
{if segment_4 != ""}
{redirect="site/404"}
{/if}
You can either key that in or grab the completely updated template from the Companion Files: chapter_27/weblog/index.txt.
Let's move to the weblog/comments template. Because this template will only be used to display single entries, we can use the same approach as we did with products/detail and services/detail - with a top-of-template check against junk in the fourth segment, the require_entry parameter and {if no_results} conditional within the main {exp:channel:entries} tag pair:
Template Name: weblog/comments
{if segment_4 != ""} {redirect="site/404"} {/if} {embed="embeds/html_header" my_page_title="Weblog | {exp:channel:entries channel="weblog" disable="member_data| pagination|categories|custom_fields"}{title}{/ exp:channel:entries}"} {embed="embeds/page_header" the_url_title="weblog" my_location="weblog"} <div id="maincontent"> <div id="right_side"> {snp_weblog_categories} {snp_weblog_archives} {snp_latest_products} {snp_syndicate_block} </div> <div id="left_side"> {exp:channel:entries channel="weblog" Chapter 27 331 limit="1" require_entry="yes"} {if no_results} {redirect="site/404"} {/if} <div class="entry"> <h3>{title}</a></h3> {summary} {full_entry} <div class="posted"> Posted by {author} on {entry_date format='%m/%d'} at {entry_date format='%h:%i %A'} in {categories} <a href="{path='weblog'}">{category_name}</a> • {/categories} {if allow_comments} ({comment_total}) Comments {/if} </div> </div> {/exp:channel:entries} {embed="embeds/entry_comments" the_channel="weblog"} </div> </div> {snp_footer}
Companion Files: chapter_27/weblog/comments.txt
With this code in your weblog/comments template, you should be able to navigate to a specific single-entry on your Weblog and trigger the 404 page by either adding a junk 4th segment or an invalid third segment.
Still Using index.php?
One final tip if you plan to leave the EE index.php in your URLs (either as-is or renamed): You'll also want to cover the possibility of invalid URL content before the index.php file. For example:
http://yoursite.com/junk/index...
Since the invalid URL appears before the index.php file ExpressionEngine won't get this request and won't serve up the 404 page. What you'll usually get is whatever your web server does by default.
Figure 120: 404 for Non-ExpressionEngine URL
What you can do here (if you are on a Unix web server and have access to your .htaccess file located in the web root) is to put rules in the .htaccess to send 404 requests to your ExpressionEngine 404 template:
ErrorDocument 404 /index.php/site/404/
ErrorDocument 500 /index.php/site/404/
ErrorDocument 403 /index.php/site/404/
With these in place, you should see your styled 404 page instead:
Figure 121: Styled 404 for Non-ExpressionEngine URL
The Results
At this point, you should have 404 code in all the templates used to deliver the content in the main sections of your project site. The only templates we didn't touch were anything in the embeds and search Template Groups - these templates shouldn't need 404 code added.
You should have a site with much tighter 404 control, which is important if your project is a redesign of an existing site where the URLs will all change.
Not Working?
If you are having trouble with the 404 management covered in this chapter, here are some tips:
- Make sure your code is looking at the proper segment position - especially if you've copied code from another template.
- Remove all 404-related code, navigate back to the section of the site, look at the structure of the URLs generated and try to see how both valid and invalid URLs would look. See if that changes your approach.
- Make sure you've got Strict URLs turned On.
- Make sure you've specified the correct template for 404 use.