Final Touches - Contact Form, SEO Metadata, Error Handling & Performance Optimization

<<Previous Lesson      

What a journey it’s been! We’ve gone through the entire process of planning and building a Portfolio Website, but we’re not quite done - there are a few more things worth including in this introduction to building in ExpressionEngine. These are things you probably need in any production site, so let’s finish strong.

In This Article:

Step 1: Adding a Contact Form

Our contact page at is nice, but it’s not very interactive. Let’s add an interactive Contact form so users can send a message directly from the site rather than having to use their native email client. For this, we’ll use the Email Contact Form which is part of EE’s included Email Add-on

  1. In the CP, go to Developer > Add-ons or use the Jump Menu and type "view add" to get to the Add-ons screen, then scroll down in the list to find "Email" under the ‘Uninstalled’ heading and click Install:

    Email addon

  2. Open up your contact/index template, and let’s rearrange what we had set up earlier.
  3. Put the form where the map was and put the map underneath - move the {map_embed} code underneath the closing </div> that closes the ‘row,’ then copy & paste the example straight from the docs inside our right column (i.e., the second "col-md-6" div).
  4. Update the recipients parameter to your email for testing and anything else you’d like to update - I’ve replaced the value parameters on the form fields with placeholders instead and added a class to the button, so my updated contact/index template now looks like this:
    {exp:channel:entries channel="contact" limit="1"}
      <h1>{if page_heading}{page_heading}{if:else}{title}{/if}</h1>
      <div class="row align-items-center">
        <div class="col-md-6">
              {if address:line_2}<br />{address:line_2}{/if}
              <br />{address:city}, {address:state} {address:zip_code}
            {if departments:count == 1}<h2>Departments</h2>{/if}
              <a href="tel:+1-{departments:phone}">{departments:phone}</a>
              <a href="mailto:{departments:email}">{departments:email}</a>
        <div class="col-md-6">
                {exp:email:contact_form user_recipients="no" recipients="" charset="utf-8"}
                  <h2>Send Us A Message</h2>
                    <label for="from">Your Email:</label><br />
                    <input type="text" id="from" name="from" size="40" maxlength="35" placeholder="Enter your email..." />
                    <label for="subject">Subject:</label><br />
                    <input type="text" id="subject" name="subject" size="40" placeholder="Enter a subject..." />
                    <label for="message">Message:</label><br />
                    <textarea id="message" name="message" rows="6" cols="40" placeholder="Enter your message..."></textarea>
                    <input class="button" name="submit" type='submit' value='Submit Form' />

    NOTE: If you’re working locally, it’s unlikely emails will send directly from your test site without jumping through a whole bunch of hoops. In most hosted environments, though, this works just fine because the default setting is to use PHP mail as the sending protocol. If you’d prefer, you can use Sendmail or SMTP by changing the settings in Settings > Outgoing Email or using the Jump Menu to find ‘Outgoing Mail.’

    Make sure to read through the documentation for Email Contact Forms, paying close attention to the multiple warnings there if you plan to use this in production. This is a very basic example, and you’ll probably want to build this out a little differently, add Captcha or perhaps even use a third-party Add-on such as Forms or Freeform.

Step 2: Add SEO Metadata

Right now, all of our pages use the same <title> tag because that’s hard-coded in our site/_wrapper template. This is awful for SEO, and we should update our code so at the very least, the Entry’s Entry Title is used as the <title> tag. Even better, we can give content editors the ability to overwrite the default to customize the SEO Page Title, and while we’re at it, we can let them update other metadata like the SEO ‘description’, ‘keywords,’ etc. If you’re not sure what I mean by ‘metadata’, it’s worth researching that, but for our purposes, I’ll be including two:

All of our Channels have content with unique URLs, so each one needs the same functionality. If you had any Channel-based content that didn’t have unique URLs (think maybe testimonials or team members that output all on one page), those Channels wouldn’t need these.

The high-level plan here is to create these two fields, get them assigned to each of our Channels, and then update the templates to utilize them. Remember that Fields can be shared across Channels and even assigned to a Field Group, which is perfect for this scenario.

  1. In the CP, use the Jump Menu to create a new Field Group (or go to Developer > Fields and use the blue button). Don’t assign any existing fields because we need to create new ones.
  2. Once created, click on "SEO" in the left ‘Field Groups’ sidebar, then click the blue New Field button. Selecting the "SEO" group first means the new fields we add will automatically be assigned to this group.
  3. Add two fields, making sure to first click the "SEO" group before adding each one:
    • SEO Page Title (Text Input, add Instructions: "Overwrites the default Entry Title as the page’s Title tag.", Include in search, Max characters = 60)
    • SEO Description (Text Input, add Instructions: "Sets the SEO Meta Description - for Work entries this replaces the default Excerpt as the Description.", Include in search, Max characters = 160)
  4. Go to Developer > Channels (or use the Jump Menu typing in "edit chan," then select Edit Channel titled [channel] then choose a channel to edit) and for each Channel:
    • Edit the Channel by clicking the Channel name
    • Go to the Fields tab and check to select the "SEO" Field Group
    • Save & Close the Channel
  5. Tidy up the Publish Layout for each Channel by:
    • Either: a) Go to Developer > Channels then click the ‘Layouts’ icon to the far-right of each Channel name b) Use the Jump Menu, type "view pub", select View Publish Layouts for [channel], then select a channel
    • Edit the existing layout or create a new one if you haven’t already
    • Add a new Tab called "SEO"
    • Drag the two SEO fields from the Publish tab into the new SEO tab, order them how you want then and save the Publish Layout

      NOTE: When dragging fields to other tabs here, make sure you see the blue line under the new tab’s name before you let go:

      Dropping fields in a different Publish Layout tab

  6. In your text editor, open up the site/index template where we area going to define Layout Variables to pass up to our layouts/_wrapper template. Just inside the ‘homepage’ {exp:channel:entries} loop before our {hero_carousel} variable pair, add these layout variables:
    {layout:set name='seo_title'}{if seo_page_title}{seo_page_title}{if:else}{title}{/if}{/layout:set}
    {layout:set name='seo_description'}{seo_meta_description}{/layout:set}
  7. Now open the layouts/_wrapper template:
    • Replace our hard-coded content in our <title> tag to be:

      Or if you wanted to, you could have consistent, hard-coded appended text like:

      <title>{layout:seo_title} | Example Company</title>
    • Below the new dynamic <title> tag, add:
      <meta name="description" content="{layout:seo_description}">
  8. In your browser, go to to view the Homepage and note the <title> tag has updated and is now dynamic, but note there’s no Meta Description if you look at the source

  9. Go back to the CP, edit the Homepage using the Jump Menu or Entries > Homepage, switch to the SEO tab, add text to both fields, save the entry, and refresh your browser to see it’s updated

  10. Next, let’s create a Template Partial so we can re-use the Layout Variables since all our channels use the same ones:
    • In the CP, use CTRL/CMD + J then in the Jump Menu type "part" then choose View Template Partials and click the Create New button and add the name "par_page_seo_variables"
    • Cut and paste the two lines we just added to our site/index template into the Content field and save the Template Partial
    • Where those lines used to be in the site/index template, add our partial tag: {par_page_seo_variables}
    • Test the Homepage again to make sure it’s all still working

  11. Now, we can reuse this simply by adding that partial tag to each of our Single Entry templates, each time just inside the opening {exp:channel:entries} opening tag:
    • about/index
    • contact/index
    • services/index
    • work/index - this time the partial would be added just inside the {exp:channel:entries} tag that we’re using for the Work Entry detail pages, ie:
      {!-- Work Entry detail page --}
      {exp:channel:entries channel="work" limit="1" url_title="{segment_2}"}

      NOTE: You can set Layout Variables from anywhere within a template - so in this case, we’re doing in here because our partial looks for fields that are only available from inside the {exp:channel:entries} tag and are unique to each of our Work entries.

  12. Now let’s update our partial so it can set the {excerpt} to be the description by default for our Work Entries. Open up the _partials/par_page_seo_variables template and update the seo_description Layout Variable to:
    {layout:set name='seo_description'}{if seo_meta_description}{seo_meta_description}{if:elseif excerpt}{excerpt}{/if}{/layout:set}

For the work/index template, we’re able to use these SEO fields for the individual detail pages like we just added above, but our main Work landing page, and the versions where Categories are selected, aren’t actually editable… but we can still use Layout Variables to manually set these in the template - we just only want to do that if it’s not an individual Entry detail page:

  1. For when a category is selected, in the work/index template, find where we’re using the Category Heading tag to dynamically set the page heading - we can set our Layout Variables there and use the selected category’s name and description:
    {exp:channel:category_heading channel="work"}
      {layout:set name='seo_title'}{category_name} Portfolio{/layout:set}
      {layout:set name='seo_description'}{category_description}{/layout:set}
      <h1>Our Portfolio: {category_name}</h1>

    It’s possible the Category Description might not exist - you could also use a conditional here, so if it didn’t exist, you added some kind of fallback. You could even set up that fallback as a Template Variable, so it was editable via the CP.

  2. For when a category isn’t selected, i.e., we’ll use the final section of the first conditional in the template to set our Layout Variables because this part gets rendered if there’s no second segment:
      <h1>Our Portfolio</h1>
      {layout:set name='seo_title'}Our Portfolio{/layout:set}
      {layout:set name='seo_description'}Our amazing portfolio is sure to impress!{/layout:set}

    Again, you could set up a Template Variable to hold this Work landing page SEO Description if you wanted to.

That should take care of the SEO Metadata - every page on our site is now either editable via the SEO tab when editing the Entries - or set up in the templates - and wherever possible, we’re setting default values such as the Entry Title and the Work Channel Excerpt. Of course, you could extend this, add fields, use specific fields for specific Channels, etc. in a real project.

Step 3: Error Handling

Everything we’ve covered so far has been looking at how to handle legitimate requests for URLs that actually exist, but it’s important to cater for ones that don’t and handle the errors appropriately. Similarly, we also want to consider the possibility that the content could change, so what once did return content no longer does and handle that too.

ExpressionEngine’s 404 Page

If a requested URL doesn’t exist or doesn’t have any content, we want to serve a 404 page, which tells the user (and search engines) that that URL doesn’t actually have any content. EE has a built-in way to handle this that uses a dedicated template, so we first need to create a 404 template, assign that as the 404 template, then go through our templates to utilize it:

  1. Create a new file in the filesystem named 404.html in the directory. You could also go to Developer > Templates or use the Jump Menu to get to Create Template in [group] then choose the site group.

  2. In our site/404 template, add the following and save the template:
    <h1 class="text-center">Page Not Found</h1>
    <p class="text-center">Sorry, but the page you requeste cannot be found or has no content.</p>
    <p class="text-center"><a href="{site_url}" class="button">Go to the Homepage</a></p>
  3. In the CP, go to Settings > Template Settings or use the Jump Menu to get to Template Settings, then under 404 page, select the site/404 template and save.

  4. In your browser, go to and our 404 page is served

This works, but it only works automatically if there’s only one segment. Remember the way template routing works; if only one segment is present, the rendering engine looks for a Template Group with that name, and if it can’t be found, it loads the index template, and if it can’t be found, it throws an error. So by default, EE handles an erroneous single segment so long as we’ve set up our 404 template.

However, if there are two segments and the first is an existing Template Group, but the second (or any others beyond it) are erroneous, then it’s up to us to handle this because from the second segment on, there are all kinds of things you could do with segments. For example, try going to in your browser - in this case, the template work/index template is correctly being loaded and has content in it that’s legitimate, but our {exp:channel:entries} loop that’s looking for a specific Entry based that matches the second segment just isn’t returning anything… because there’s no entry that matches.

EE gives us two powerful tools to handle this:

Combining these two, we can show a 404 for content that doesn’t exist in our our templates:

  1. In our work/index template, inside the Work Entry detail page {exp:channel:entries} tag, add {if no_results}{redirect="404"}{/if} as the first line within the tag:
    {!-- Work Entry detail page --}
    {exp:channel:entries channel="work" limit="1" url_title="{segment_2}"}
      {if no_results}{redirect="404"}{/if}

    Now try again

  2. Do this same thing for all of our single-entry {exp:channel:entries} tags - i.e., wherever we have the limit="1" parameter - in the rest of our templates:
    • about/index
    • contact/index
    • services/index

    NOTE: We don’t need to do it in our site/index template because EE handles the error for a single segment not matching a Template Group automatically, but there’s no harm adding it there if you just want to get into the habit of doing this on any single-entry template.

This takes care of the single-segment URLs in the scenario that there’s nothing returned within the Channel Entries Tag, but what about if we have extra segments? Try going to in your browser - notice we still get the About page loading. EE has a way to handle this too - the require_entry="yes" parameter - but it only works in the typical 2-segment template_group/template scenario because what it’s doing is telling the Channel Entries tag that a valid Entry ID or URL Title must be found in the second segment. In the case of, we don’t want to use this because we deliberately don’t want a second segment. So we only will add this if a second segment exists:

  1. In the about/index template, add the require_entry="yes" parameter to the Channel Entries Tag, but only if a second segment exists:
    {exp:channel:entries channel="about" limit="1" {if segment_2}require_entry="yes"{/if}}
      {if no_results}{redirect="404"}{/if}
  2. Do the same thing for the contact/index and services/index templates as these templates also are designed to output a single Entry.

  3. In our work/index template, we can add the same parameter to the Work Entry detail Channel Entries Tag, but we don’t need the conditional because the whole thing is already only showing if we have the second segment:
    {if segment_2 && segment_2 !="category" && !(segment_2 ~ "/^P\d+/")}
      {!-- Work Entry detail page --}
      {exp:channel:entries channel="work" limit="1" url_title="{segment_2}" require_entry="yes"}

So now we’ve handled errors for 2-segment URLs where the 2nd segment is looking for a matching Entry but can’t find one, but we also need to consider there could be 3-segment URLs where the first two segments do match a single Entry, but where we have extra segments. In the Services and Work sections, for example, we do have legitimate 2-segment pages, and above, we added dynamic Channel Entries tags that are requiring the 2nd segment to be a valid entry… but try going to and - both examples will load, so it’s possible to load the same content at multiple unique URLs. Our site structure only ever uses a third segment in the Work section where we’re filtering by category (e.g.,, so outside of that specific scenario, we want to force a 404 if there’s ever a third segment:

  1. In the services/index template, add a 404 redirect if a third segment is present:
    {if segment_3}{redirect="404"}{/if}
    {exp:channel:entries channel="services" limit="1" {if segment_2}require_entry="yes"{/if} {if !segment_2}status="Default Page"{/if}}
  2. In the work/index template, find the conditional where we’re checking if a second segment exists, but it’s not "category" and do the same:
    {if segment_2 && segment_2 !="category" && !(segment_2 ~ "/^P\d+/")}
      {if segment_3}{redirect="404"}{/if}

The final place to consider using a 404 is in the Work landing page when using category filtering and either:

E.g., try and also - both of these scenarios we want to redirect to a 404. Remember that pagination also uses a segment, so if a category is selected and there’s pagination, we do need that 4th segment to work, e.g.:, but we can cater for this using the regular expression condition we used earlier, i.e. !(segment_4 ~ "/^P\d+/").

  1. In the work/index template, add a 404 redirect at the very top of the template if there’s ever a 4th segment that doesn’t match the pagination regular expression:
    {if segment_4 && !(segment_4 ~ "/^P\d+/")}{redirect="404"}{/if}
  2. Then we can use Category Heading Tag we’re already using to make sure that the category in segment 3 is an existing category again using the {if no_results} conditional:
    {if segment_2 == "category" && segment_3}
      {exp:channel:category_heading channel="work"}
        {if no_results}{redirect="404"}{/if}

And that should take care of all of our 404 redirects!

No Results In ‘Visiting Content’ Output

Now that we’ve considered all the possible erroneous URL errors and handled them with the 404 page, we should also cater for the few places in our site where we’re pulling through content within a page in the scenario that no results are returned for whatever reason. The places we need to consider in this site are:

  1. In the Homepage template (site/index), find where we’re outputting the ‘Latest Projects’. To handle there being no results, you could either:
    • Add an {if no_results} conditional inside the Channel Entries:
      {exp:channel:entries channel="work" limit="3" dynamic="no" orderby="date" sort="desc"}
        {if no_results}<p>There are no entries to display</p>{/if}
    • Or perhaps more appropriate, pull the surrounding <h2> and <div class="row"> HTML to be inside the loop and use the {count} and {total_results} variables in conditionals to maintain their positions. This way, if no entries are returned at all, that whole section doesn’t display:
      {exp:channel:entries channel="work" limit="3" dynamic="no" orderby="date" sort="desc"}
        {if count == 1}
          <h2>Latest Projects</h2>
            <div class="row">
              <div class="col-4">
        {if count == total_results}
  2. For the Work landing page, we will use the {if no_results} conditional, but we can kill two birds with one stone because the same Channel Entries Tag outputs the Entries regardless of if a category is selected, but we’ll cater for that with a conditional inside our text:
    {!-- Work landing/listing page --}
    {exp:channel:entries channel="work" limit="3" paginate="both" orderby="date" sort="desc"}
      {if no_results}<p>There are no Projects {if segment_2 == "category"}in this category{/if} to display at this time.</p>{/if}

Step 4: Performance Enhancements

You can (and should) do a few things right from the beginning when learning to build in EE to optimize performance. The ultimate goal is to get your site’s pages to load as quickly as possible for the end-user, and there are two parts to that: server-side and client-side. The server-side part happens first, and essentially is the time it takes for the CMS to build up and serve the final HTML page and send it to the browser. Then the client-side involves all the things that the browser does once it receives the HTML for the requested page - usually the majority of the time is spent loading assets (images, CSS, JavaScript files, etc.) and executing any of the client-side scripts. This section will focus exclusively on the server-side part - things we can do in EE to make serving the HTML files faster - and this isn’t intended to be an extensive performance workshop, but rather give you the basics you should start doing right from the start.


Before we dive into specific things to do to optimize performance, it’s worth knowing how to test it. EE comes with built-in debugging, which you can enable at Settings > Debugging & Output > Enable Debugging. Enabling this toggle adds additional information to the bottom of the template output, but only for SuperAdmin members, with really useful information like ‘Memory usage’, ‘Database execution time’, and ‘Total execution time.’ There’s also a ‘Variables’ tab showing the values of various variables (most of them specific to the logged-in user) and a way to view all of the individual database queries. For the most part, you can use the ‘Total execution time’ as a benchmark while testing and seeing how much of a difference implementing certain changes make, but it’s definitely work knowing about this in case you find yourself needing to investigate a troublesome page.

  1. In the CP, go to Settings > Debugging & Output, enable Debugging, save, then go to,, and and compare the debugging info between templates.

NOTE: Your server environment, specifically the load at the exact moment you make a request, could well be playing a big part in this, so if you’re really digging into this, try refreshing the same page several times to try and identify any outliers.

Looping and Embedding

We’ve only briefly mentioned this and haven’t actually used it in this tutorial, but it’s possible to ‘Embed’ templates within other templates. This can be really useful, particularly given you can pass ‘Embed Variables’ from one template to the one you’re embedding into it, and in some cases can be necessary to allow the rendering engine to access and render certain things in a certain order. The rendering order is a more advanced topic I deliberately am not covering - if you want to dive deeper or encounter a scenario where something’s not rendering where you want it, read up on the Rendering Order of the Template Engine.

But what I will do is point out you need to be very careful when embedding templates into other templates. Embeds aren’t that expensive from a performance perspective in their own right, but what you do with them absolutely can be! If at all possible, avoid embeds within any kind of loop, especially if the embed contains a Channel Entries Tag.

The Channel Entries Tag is one of the heaviest tags you’ll use in EE in terms of performance - it typically does the most work, so if you have an outer loop with 20 iterations where within that loop, you embed another template that has a Channel Entries Tag that has 20 iterations, your page runs the Channel Entries Tag 20 times and loads 400 entries! So be careful and do your best to build in the most efficient way possible.

Granted, it’s a pretty simple site, but all of our code in this tutorial is pretty efficient - we don’t have any loops within loops, nor are we embedding any templates anywhere.

Channel Entries ‘disable’ Parameter

We just mentioned the Channel Entries Tag is one of the heaviest tags you’ll use, but it has an important parameter available to it that allows you to disable things you don’t need in order to speed things up. Think about it in terms of database queries - the less information that needs to be included in the requests from the database, the faster it’ll be. Including unused information in many cases doesn’t add that much extra time, but remember this tag often loops through multiple results, so anything we can save is worthwhile. So always use the disable parameter on Channel Entries Tags, disabling anything you’re not explicitly using within the tag. As with other parameters that accept multiple values, use a | (pipe) character to add multiple values. Here is an example with all of the available options - you can remove the ones you do need before adding to the {exp:channel:entries} tag:


NOTE: category_fields are custom category fields - you can still access the default fields of a category like category_name, category_url_title, category_description and category_image with the disable="category_fields" set. Also, disabling categories automatically also disables category_fields. Even if you need categories, chances are you want to still disable category_fields unless you need a specific custom category field in the tag output.

  1. Add appropriate disable parameters to all of our Channel Entries Tags:
    • site/index -
      • disable="categories|member_data|pagination" for the Homepage Channel Entries Tag
      • disable="categories|member_data|pagination" for the Work Channel Entries Tag
    • about/index - disable="categories|member_data|pagination"
    • contact/index - `disable="categories|member_data|pagination"
    • services/index - `disable="categories|member_data|pagination"
    • work/index -
      • disable="category_fields|member_data|pagination" for the Work Entry detail page Channel Entries Tag
      • disable="categories|member_data" for the Work landing page Channel Entries Tag

        NOTE: Disabling categories affects information returned for each item inside the loop, not filtering the loop itself - so even with categories disabled here, still renders what we want


It’s absolutely worth reading EE’s documentation on Data Caching & Performance for full details if you want to really optimize performance. The main takeaway is we can (and should) add caching to our EE builds for any template output that returns the same data for every visitor. Now, if you have data specific to the logged-in member, you have to start being careful with this, but in this tutorial, we can implement caching on pretty much everything.

Tag Caching

The first type of caching is Tag Caching where we can cache and {exp:xxxxx} tag for a set amount of time using cache and refresh parameters, For example, on a Channel Entries Tag, you might add:

{exp:channel:entries ... cache="yes" refresh="60"}

The number here is the time in minutes, so the above would cache the output of the entire tag for 60 minutes. The general consensus is if any {exp:xxxxx} tag you use is the same for each visitor, add caching to it.

  1. Add the cache="yes" and refresh="60" parameters to every {exp:xxxxx} tag in all of our templates, with the exception of the {exp:email:contact} tag in contact/index - that one we don’t want to cache to avoid any possible crossed wires. Make sure to include the {exp:channel:category_heading} and {exp:channel:categories} tags in work/index though.

To see how effective the disable, cache, and refresh parameters are, choose a template and remove all these parameters, then load the page and note the ‘Total Execution Time’ in the debugging info. Then put them back and refresh that same page again twice - the first time you reload after re-adding the parameters is the rendering engine’s first time loading the entry where it actually creates the cache, so it won’t be much different to the performance without the tags… but the second time, where the tags are loading from cache should be a significant difference.

On my Homepage, I went from 0.1964 seconds to 0.0357 seconds!

Template Caching

The second type of caching is Template Caching, which literally caches the entire template, not just the tags inside of it. With this option, you can’t pick and choose which tags to cache, but it can save a little bit more time. You only want to use this where the entire template’s output would be the same for each visitor - in other words, if you’re caching all the individual tags inside the template, you should go ahead and enable this at the template level.

  1. In the CP, use the Jump Menu to get to View Templates (or go to Developer > Templates), then click the little ‘settings cog’ icon next to the site/index template, then turn on the Enable Caching? toggle and set the Refresh Interval to 60, then refresh your homepage a couple more times and note the ‘Total Execution Time.’

    This dropped my Homepage to 0.0304 seconds - it’s not a huge difference, but our goal is to the best we can, so this helps!

  2. Repeat this process the following templates:
    • about/index
    • services/index
    • work/index

    NOTE: We’re not doing this on contact/index because we don’t want to cache our {exp:email:contact_form} tag.

Advanced Performance Tuning

If you want to get more advanced with performance optimization, take a look at TJ Draper’s "Tuning For Performance" presentation from the EE Conference (both 2017 and 2018). You can watch the video here and follow along with the slides and resources here.

Justin Alei's avatar
Justin Alei

Justin is an expert in ExpressionEngine and has created several award-winning sites for Consumer51 on the platform.