Setting Up Custom Category URL Structures in ExpressionEngine

by Jonathan Longnecker

We just finished up a particularly fun project, Casillas, Inc. They’re an amazing custom furniture maker in California. In an odd turn of events, we actually did very little design for the site and became the “backend guys” if you will. As we got into how we wanted the site to be structured we realized that because of the product names (all numbers) we needed to use the category that the product was part of in the URL structure. So for example it would read:

http://site.com/products/sofas/10-123/

And if the user accesses:

http://site.com/products/sofas/

Then all the sofas get shown. Back one more and all the categories. Great! Now how in the world do we do that with ExpressionEngine?

First Problem

By default, EE uses either the category ID number to display all the entries in that category:

http://site.com/products/C12

Or you can turn on URL titles and choose your “trigger” word. So if category was the trigger the URL would be:

http://site.com/products/category/sofas

Hence, we need to figure out how to remove the category trigger.

Second Problem

We want the URL title of the entry (the product number in this case) to immediately follow the category name. And we want that URL scheme to be the permanent location of the product. Because this isn’t how EE works out of the box, there is potential to trip us up down the road and make it hard to create those permalinks reliably. Can we do it? You bet we can!

Category URL explanation

The Solution

As you can see in the diagram above, we’re going to harness the power of segment variables to make our magic happen. What are segments? Well, it’s pretty easy. Segment 1 is the first section in your URL after your .com, Segment 2 is the next and so on. EE will handle up to 9! I hope I hope I never have to use that many.

First, we need to setup our template group (products in this case) and then on the index template inside that group we begin our work. We’re actually going to manage to put all of this in one template using if statements. Yep, that blew my mind the first time, too. Keep in mind that all this code has been stripped down to bare minimum to keep it easier to follow. My template code for this page was pretty large with all the HTML markup in it.

We’ll start with the categories page:

{if segment_2=="" AND segment_3==""}
{exp:weblog:categories weblog="products" style="linear"}
    <p><a href="{path="products/{category_url_title}"}">
    {category_name}</a></p>
{/exp:weblog:categories}

Here we tell EE that if Segment 2 and 3 are empty, we use this part of the template. Then we use the weblog:categories tag to list the categories and make them links.

Second is the category entries page:

{if:elseif segment_2!="" AND segment_3==""}
{exp:query sql="SELECT cat_id FROM exp_categories WHERE 
cat_url_title = '{segment_2}'"}
    {exp:weblog:entries weblog="products" category="{cat_id}"} 
        <p><a href="{title_permalink={segment_1}/{segment_2}}">
        {title}</a></p>
{/exp:weblog:entries}
{/exp:query} 

Here we continue the if statement by saying that if Segment 2 has something in it and Segment 3 is empty we use this part of the template. Add to that a little SQL Query magic to pull the category ID from the category URL and then you can start making your links to the products. Notice you need to pass the category ID to the weblog loop and then you can use the title permalink in conjunction with the first two segment variables.

And finally the single entry:

{if:elseif segment_3!=""}
{exp:weblog:entries weblog="products" limit="1" 
url_title="{segment_3}"}
    Call whatever custom fields you have here
{/exp:weblog:entries}
{/if}

If Segment 3 is populated then we look for the entry inside it and pull the data. It gets more complicated after that to use permalinks and title paths, but we didn’t run into an issue we couldn’t get past. And don’t forget, you can go to http://casillasinc.com/products/ and click around to see how it works. Also, before you think I’m a mad genius or something, this is based on some excellent work by a guy named “TheStig” in the EE forums. The only credit I can take is for making it a little easier to understand wink.

Due to line breaks and such I’ve put all of the above together for you in one file so it will be easy to get started. Have fun!

Download: the template code.

Edit: Be aware that next/previous linking and pagination won’t work with this method. For next/previous linking this might help: Nearby Entries

May 20, 2009

Design, ExpressionEngine

Comments

  1. Nice solution guys! Thank you for sharing this - I’m on the same stage, dialing with categories and category segments

  2. I really like the insight you guys give into your work flow and design process. It’s always a pleasure reading blog posts such as this. Please keep them coming.

  3. Nice post.

    Isn’t the following a little redundant?

    {if segment_2==”” AND segment_3==”“}

    If segment_2 has nothing, then 100% of the time segment_3 will have nothing. You many only need to check for something in segment_2 there.

  4. Thanks for taking the time to share that.

    I tend to put in a closing if:else in to cover any unexpected glitches or unforseen enventualities; but then I’m a bit of a belt and braces sort of guy… wink

  5. Why not use if !segment_2 AND !segment_3, this basically does if NOT segment, i.e if segment doesn’t exsits.

  6. @Ryan It made sense to me at the time, but you’re probably right about not having to check for 3. I’ll have to test it out. @Josh interesting..is that a sort of shorthand? I hadn’t run across that before.

  7. awesome! thanks. that answers up some quasi nagging questions i’ve had about the url structures in EE.

  8. I’ve been using this exact same method for quite a few category-based sites recently.

    The other method I’ve also used to achieve the same sort of thing is to use the Structure module from nGen Software.

  9. In this case, using Low Seg2Cat would help with the use of {segment_2_category_id}. Then you don’t even need to use the query module.

  10. I agree about Low’s Seg2Cat - you’ll never need the query module for this sort of thing again if you use that very handy extension. Your category entries block would be as simple as:

    {if:elseif segment_2!=”” AND segment_3==”“}
      {exp:weblog:entries weblog=“products” category=”{segment_2_cat_id}”}
          [url=”{title_permalink={segment_1}/{segment_2}}”]
          {title}[/url]
    {/exp:weblog:entries}

  11. Man, I’m telling you there are so many modules/plugins/extensions that I’m just now running across that could have made my life easier. EE’s really starting to mature as a platform. Thanks for the Low Seg2Cat tip guys!

  12. Pretty slick method to get that done, and great reading through the comments to come up with a few ways to refine it! Thanks for the post.

  13. Great writeup and another shoutout for Seg2Cat - one thing I’ve run into handling categories this way is having to add a bunch of conditionals for the search results (and google sitemaps) to handle the custom paths correctly and insert the category name. Just curious how you’re handling that in the search results on this site?

  14. @Mike All I did was use the categories tag in the href path:

    {categories limit=“1”}
    {site_url}products/{category_url_title}/{url_title}/
    {/categories}

    Granted, I’m not sure how that would work if more than one category was assigned to an entry, but in this case that won’t happen so it’s cool.

  15. Great article!  Exactly what I’ve been looking for, but I’m having a hard time getting rid of the “index.php” before the template_group.  I normally use the “remove index.php” .htaccess method, but it’s not working with the “path”.

    What am I missing?

  16. I had to go to Control Panel Home › Admin › System Preferences › General Configuration and delete the name of the site’s index page.

    It’s working great now…

  17. Actually, no that didn’t work.  It removed the index.php, but the page quit working.  Sorry for blowing up your comments section.  I know this isn’t a forum.

  18. Hah no problem earph smile. So I would take it back to basics first. Can you access something like http://site.com/template_group without the index.php?

  19. False alarm… removing the index.php from the system preferences DID work!

  20. Sweeeet glad to hear it!

  21. I noticed that {exp:weblog:next_entry} & {exp:weblog:prev_entry} won’t show up in the “single entry” section.  I assume this has something to do with the URL path.

    Any ideas on how to get it work?

  22. I don’t see why it would have anything to do with that. Have you checked out the docs page on them?  I noticed in one of the comments it said they have to be outside of the weblog loop. http://expressionengine.com/docs/modules/weblog/entry_linking.html

  23. Yes, I did see that and tried it with the tags outside of the weblog loop, but didn’t fix it.  Kind of weird, huh?

  24. Did you try it on a plain vanilla test page? Include all the parameters? You might have to tell it that the url_title is in segment 3 like we did in the weblog loop above.

  25. Yes, I’ve tried {exp:weblog:next_entry url_title=”{segment_3}, both within the main weblog loop and outside the loop.  No dice.

  26. Sorry, I’m flying blind here without code, but you might want to make sure you’re passing the weblog name, maybe entry ID, limit, etc…

    I’ve honestly never used the next/previous links like that before so I’m not sure what their limitations are.

  27. Technically, next_entry and prev_entry should only show up on single entry pages.  Since the if statement is creating the URL dynamically and only swapping out template code, perhaps the system doesn’t recognize the page as a “single post” and therefore doesn’t display the next/prev links.

  28. That’s very possible; sorry I haven’t been more help :(.

  29. i have tried out but wont work. Help me a bit
    Thanks

  30. @Honest I don’t really know what’s not working for you?

  31. Great post! This works great for simple cases where there is only a top-level category that each post belongs to…. I do have a question however…

    How would I go about doing this for posts that are listed in multiple categories? What about for sub-categories?

    Thanks

  32. @Mike For multiple categories I honestly have no idea unless you somehow just had it pick one, but depending on what you’re doing that’s not very helpful. For sub-categories, check out the link in my article to the forums. I believe the original version had support for sub-categories using a plugin or extension.

  33. right guys, I’ve played around and found that Next / prev entry linking is not working as the pagination on a category view, which is maybe because the dynamically builded segments.

    This becomes serious issue for me as I keen to use the next / previous and pagination on a product catalog

  34. @Vlado, you may want to file a ticket/bug with the EE guys. Like I said, this obviously isn’t the standard way of putting pages together so they would know why it isn’t working.

  35. I’ve started a few threads in EE forum in regards to the “next/prev” issue, but have yet to find a solution.  Most of the threads have ended with the conclusion that it’s just not possible.  I’ve tried alternate methods, but no luck there either.  If you any of you figure out this problem, PLEASE let me know.

  36. Hey earph, is there not a way to pull a SQL query and just manually grab it out of the database?

  37. Possibly, the problem is that I’m not that technically advanced (more of a css guy) and wouldn’t know how to pull that off.  It sounds like it would work though, if you have an idea on how to do it.

  38. I’m afraid I’m not that advanced either. I’m like you, more of a HTML/CSS guy who uses EE and attempts to bend it to my will, but when it comes to writing SQL queries and PHP I don’t understand the syntax well enough. That’s why I love me some EE forums!

  39. Boom! (sound of my head exploding). Thanks.

  40. How did you remove the ‘category’ trigger? (Or am I missing something obvious)

  41. You technically don’t remove it; you just bypass it with the if statements and sql query.

  42. Hi.

    Thanks for this, i have been looking for a way to sort the URL’s out like this. And to get an insight into your project is also useful, always keen to see how other people overcome similar problems.

    Just a heads up, the search bar on the products page is half cut off: http://casillasinc.com/products/

    Thanks again!

  43. What’s your current thinking on use of category URL’s then Jon or anybody, tool of choice?
    I also don’t have access to URL rewriting and have to use the ? query strings on setting in the CP. Is this a problem with anything mentioned here or in the comments?

    I recenlty started using blogengine.net and something that stood out right away is that it has nice categories tagging that is easy to use.
    I’m far from being a big dotnet fan though.

  44. @Ty, sorry man I’m not really following what your question is. Category URL’s are cool, but only really if you’ve got one category assigned to each entry. Otherwise you end up with duplicate content, right? I think it’s a very specific case.

  45. Hi,

    I made category structure like this, but now I am facing pagination.
    Can you guide me how to add pagination code to this code.

    Thanks,
    Tejas

  46. @Tejas, based on this comment thread pagination and next/previous linking don’t seem to work with this method. In theory some sort of SQL Query could help you find and link to the next post, but I don’t know the syntax. Sorry I can’t be of more help.

  47. On the Categories Entries page what variable are you using to return the h1? For example the “Sofas” heading on http://casillasinc.com/products/sofas/

    Is the documentation of this somewhere, I looked briefly but could not find anything?

  48. @brad we’re using something to the effect of this:

    {exp:weblog:entries weblog=“weblog_name” category=”{cat_id}” dynamic=“off” orderby=“title” sort=“asc” style=“linear” limit=“1”}<h1>{categories limit=“1”}{category_name}{/categories}</h1>{/exp:weblog:entries}

    Kind of manual, but it works smile

  49. Right, that’s what I was forgetting the {categories} variable pair.

    I found another solution right after I asked (always happens). I adjusted the query to also select cat_name and print that out before the weblog:entries tag. i.e.

    {exp:query sql=“SELECT cat_id, cat_name FROM exp_categories WHERE cat_url_title = ‘{segment_3}’”}
    <h1>{cat_name}</h1>

    Thanks.

  50. @brad, Nicely done, sir! That’s much cleaner.

  51. Thanks for sharing! I needed this today and thought of this exact way of doing it, but then there was that sneaking feeling of “Maybe there’s a better way?” .. a Google later and at least I have confirmed that it’ll work grin

  52. looks great.

    I have an question.
    I have made a products weblog with category,
    ex: web/products/list/category/items name..

    and I want customiz same your product url,
    web/products/items name/

    how do I do ?

    would you share, how to made products web pages ?
    how to creat weblog ? category? template? custom fileds….

    thanks a again.

  53. @vincent I’m not quite following you. There’s code samples above in the article showing how to set it up. The template/custom fields will be totally different for every store. That’s why we use EE smile

  54. I’ll echo earph’s earlier comments, your next and previous weblog tags will fail to work with this method.

    You also need to add some fiddly php on input to get pagination to work otherwise the /P1/P2/Pn segments that EE suffixes to whatever segment you’re on will muck up your logic.

  55. @Seb, Right, though EE’s next/previous setup is pretty fiddly anyway wink Well, limited I guess. If you’re going to be putting together a store that needs pagination and next/previous linking this probably isn’t the solution for you. Like we said, a pretty limited case scenario. Thanks for reading!

  56. hi, Jonathan , that’s my fail.
    I try to use EE but it base on template system.
    the url look like http://web/products/list/category/itemName,
    I want replace the url to http://web/products/categoryName/itemName
    EE no any google way to do this, so I Google it then find your solution.

    now I got it to work. thanks a againg.
    using segment url , I fine 2 problem.
    1.next/pre article tag not work
    2. track_views

    not work.

  57. @vincent those tags won’t work because of the setup. Take a look at Nearby Entries, though. Should help with next/prev links at least. http://engaging.net/products/nearby-entries

  58. @jonathan, thanks.

  59. Hi Jonathan,

    I’ve followed your tutorial and it works great.

    Only thing, I want a mega drop down in my menu where users can have all the categories and every entry of each displayed below. I figured out that it would work with category_archive but I can’t see how to put the proper urls

    I’ve tried with category_heading and the entry tag, the category_archive tag, the category tag, the technique that you gave for search results, ... but had no luck with that :(

    Any ideas how ?

    Thanks !

    Any ideas how ?

  60. @Willa With this setup the category URL’s are changed so you’ll need to construct them yourself. We used something like this:

    {categories limit=“1”}
    {site_url}products/{category_url_title}/
    {/categories}

  61. Hi Jonathan and thanks for the response, works great smile

    Now that I have a big list of entry titles, do you know how I could organize them into there different categories ?

    So far I have for my drop down menu :

    {exp:channel:categories channel="products" class="nav_menu"}
    <a href="{path=" class="current"><span>{category_name}</span></a>
    {/exp:channel:categories}

    {exp
    :channel:entries channel="products" category_group="1"}
    <a href="{categories limit=“1”}{site_url}gammes/{category_url_title}/{url_title}/
    {/categories}"
    >{title}</a>
    {/exp:channel:entries} 

    How would you create a list of each categories with there related entries below ?

    Cheers,

    William

  62. @William I believe you can do something like this:

    {exp:weblog:categories weblog=“weblog” style=“linear”}
    <h3>{category_name}</h3>
    {exp:weblog:entries weblog=“blog” category=”{category_id}”  dynamic=“off”}
    <h4>{title}</h4>
    {/exp:weblog:entries}
    {/exp:weblog:categories}

    That should put the entries under their respective header. Hope that helps!

  63. Awesome !!! Thank you soooo much

  64. @William, cool. I used to do all that manually (inserting static category ID numbers) but like half my class at the last EECI told me I could just do this. Works great!

  65. Hi Jonathan,

    Thanks for the great code. I’m a bit of an EE newb and building a fairly complicated site and really want a URL structure like this. It works a treat!

    I do have a couple of questions:

    1. Giving the method you are using, how are you building your breadcrumb?

    2. Using your method, if you needed to have some single products at say, the top level (Products > 10-164) and some at a sub-level (Products > Sofas > 10-175), could your method cope with this? If yes, how would you do it as I’m very confused confused

    Thanks, Richard.

  66. @Richard, For this site, the breadcrumbs were kind of manual. Here’s the 3 types for products:

    1. Categories Entries Page

    <a href="{site_url}">Home</a>
    <
    a href="{site_url}products/">Products</a>
    {exp:weblog:entries weblog="products" limit="1" category="{cat_id}" dynamic="off" orderby="title" sort="asc" style="linear"}{categories limit="1"}{category_name}{/categories}{/exp:weblog:entries} 

    I can use the {cat_id} because of the sql query (see in article above)

    2. Product Page

    <a href="{site_url}">Home</a>
    <
    a href="{site_url}products/">Products</a>
    {categories limit="1"}<a href="{site_url}products/{category_url_title}/"{category_name}</a>{/categories}
    {title} 

    I’m not sure about your second question. I would guess you could manage some sort of if statement to check for the presence of a category and show one breadcrumb or the other? The main problem is that this whole setup assumes that each product is assigned to only one category. It kind of falls apart otherwise.

  67. Thanks for getting back to me so quickly.

    I’m doing the breadcrumb a little different, but not much. Was just curious if you managed to create it completely dynamically.

    Regarding my 2nd question, I’m still getting stuck myself. I’m actually using separate weblogs for the top level product departments and then using categories when needed.

    Think I may change this to use one weblog (products) and create categories and sub-categories, though still think I might run into issues.

    Thanks for your help.

  68. @Richard No problem. Yeah with the tag breadcrumbs and static page crumbs it was just easier to do it manually.

    RE: Categories/Sub Categories that would depend on if choosing a sub-cagtegory also adds the entry to the parent category. Then you might have issues with URL’s grabbing only one category. I’m not entirely sure smile

  69. Great writeup! Let’s assume on first landing page with the categories I also wanted to show a random product, almost like a dynamic featured product. Getting it to show up isn’t a big deal, but how would one go about linking to its product page 2 segments away?

    It seems like this shouldn’t be a big deal, but I can’t wrap my head around it. Even if the product had multiple categories (which in my case, it doesn’t), the call to find it’s category could return the top/default category.

    So basically I want a product living at /products/ to link to its product page at /products/category/url-title/—not already being nested in it’s category segment, though. Possible?

  70. Figured out my aforementioned question. The EE thread lives here: http://expressionengine.com/forums/viewthread/170356/

  71. @Chris glad you figured it out. I think others have had the same question as well..at least I think I remember putting up code like that smile Good luck, sir!

  72. Thanks for this post. It’s a great work-around to something that I think should be automatic in EE. I’m running into trouble using the switch function in the second category statement. I’m guessing it has something to do with the SQL statement but I really have no clue.

    Here’s the code:


    {exp:query sql=“SELECT cat_id, cat_name FROM exp_categories WHERE cat_url_title = ‘{segment_2}#8217;”}

    {cat_name}

    {exp:weblog:entries weblog=“museum” category=”{cat_id}” orderby=“title” sort=“asc” dynamic_parameters=“orderby”}

    {switch=“one|two|three|four|five”}

    {museum_feature}

    {/exp:weblog:entries}
    {/exp:query}

    I’ve simplified this a bit so it makes sense, but I can’t seem to get the switch to work here. Is there an easy workaround that you can think of?

  73. @Andy I had to use Switchplus; hopefully that helps!

  74. This is great and all, but destroyed when making an article span over multiple pages, but also parameter view_count_x will not work either. Such a drag.

    It would be nice to force dynamic=“on” when pulling a single entry by the url_title parameter.

  75. @Bransin, yes this is a very specific use case. Things like next/prev, pagination all stop working unfortunately.

  76. First, great EE code! I’ve been using it quite a bit.
    Second, I’m having a similar problem to Andy Krier’s with the {switch} tag. Apparently it doesn’t work inside of the {exp:Query} tags (on the Category Entries page). I also couldn’t find the “SwitchPlus” plugin anywhere, and I’m also on EE 2.0. Any help/ideas? Thanks!

  77. Hey Richard,

    Glad you’ve found this setup helpful! We obviously wrote this a while ago, so I’d check out http://devot-ee.com/ for something like switchplus. There might even be a better way to get the category other than the sql query.  Check back through the comments if you have a minute, I remember someone suggesting something else.

  78. Thanks for the feedback, Jonathan!

    Actually, I discovered why the {switch} wasn’t working within the {exp:query} tag. Apparently {switch} takes the count of the outer tag pair, so in order to get around it, you need to embed the template:

    {exp:query}
    {embed channel:entries template code, and pass the {cat_id} variable}
    {/exp:query}

    And this works.

  79. Ahh sweet; it’s amazing how often embedding works smile

  80. Amazing article…really helpful

    I ran into the issue of pagination failing with this however. On looking around it seems the plugin Low Seg2Cat solves this issue. Well it has so far anyway. Hope it helps someone.

    Thanks,
    Ian

  81. Good news Ian, thanks for sharing!

  82. Actually, I was wrong, the pagination still fails due to,  i think, the way the urls are structured. My limited programming skills may be at fault here but….

    Does anyone know the syntax for a conditional that can check for the url being a typical pagination url suffix and not a typical entry url?

    http://www.example.com/template_name/category_url/p10 for example:

    Something like, for example?

    {if segment_3 != contain “P**”}

    Therefore, we may have a way to show certain results/pages based on what {segment_3} suggested in the URL, with the P removed (not sure how) for example:

    {exp:channel:entries channel=“products” category=”{segment_2_category_id}” entry_id_from=”{segment_3 minus the P}” entry_id_to=”{total_results}}

    Could this work???

  83. Ho appena libro segnato il tuo blog su Digg e Stumble Upon. Mi piace leggere i vostri commenti.

  84. Hey guys !
    I was wondering how would this work with Multy Language module ? The module adds a /fr, / it or /de in the url and if I go to the product page I’ll get a 404. However if I add /fr/index.php/ i can get to the page

  85. @william if you’re using EE2 look into Freebie. Let’s EE ignore segments but you can still use them to grab valuable data.

  86. Awesome ! I’ll check it out. Thanks

  87. nice post.

  88. I don’t know if you guys still reply to these old posts, but I guess we’ll find out wink

    I came across this blogpost, looking to solve the exact same problem that you guys had. I also checked out the finished product over at Casillas, Inc, and found that their site have been hacked. Not cool, and quite sad to look at.

    Thanks for taking the time to share insight into EE and custom URL-structures.

  89. @Martin Berglund They didn’t get hacked, just went out of business. It’s a long saga, but the site does live on in another form. I just can’t tell you where smile

  90. @Jonathan Longnecker Aha, I see smile Good to know I was wrong.

  91. thanks this was very helpful

  92. May I ask that any member point to some other posts on this board subject? I want some more detailed info… Any help shall be very appreciated.

    Dietrich Huberstrauken
    http://www.bloggrid.net

    Many thanks!

  93. I just great Extension/Modulebut getting error on iaoiallnttsn im getting error after installing extension, can you helpMySQL ERROR:Error Number: 1142Description: SELECT command denied to user ‚c4f2user‚c4f4@‚c4f2localhost‚c4f4 for table ‚c4f2COLUMNS‚c4f4Query: SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME = ‚c4f2blog_parent‚c4f4 AND TABLE_NAME = ‚c4f2exp_weblogs‚c4f4 AND TABLE_SCHEMA = ‚c4f2user‚c4f4Nor i can access modul epage any more..i get this errorMySQL ERROR:Error Number: 1054Description: Unknown column ‚c4f2blog_parent‚c4f4 in ‚c4f2where clause‚c4f4Query: SELECT weblog_id, blog_title FROM exp_weblogs WHERE blog_parent = 0 AND blog_status = ‚c4f2enabled‚c4f4 ORDER BY blog_order

  94. You should not use if:elseif blocks like this - the contents of all of those separate sections are run on *every* page load because this is an advanced conditional. After everything runs, then EE parses the complex if conditionals and removes the sections that should not be shown. This will have a large impact on your load times since every page load is equal to loading all of the pages.

    You can fix this by using more simple conditionals that reference only a simple segment variable each, instead of the if:elseif and AND operators.

    Otherwise this is a nice intro to basic segment variable usage, good job!

Behold our Amazing Portfolio

Check it Out