Learn

Share Your Knowledge!

This fast-growing section of our site is the new one-stop shop for educational materials for ExpressionEngine with rich and expert content, from both EllisLab and the community. Submit your article, tip, or solution today!

  • Downloadable iCalendar Files

    in: Tips, Tutorials, Templating

    This tutorial will show you how to create downloadable iCalendar files. With ExpressionEngine, it’s easy-peasy because you can use your well-structured content however you need. You can use the same entries you use for your web site to create shareable .ics iCalendar format files. Here’s how in three easy steps.

    1. Install the “Download Content” plugin.
    2. Create your iCalendar format template.
    3. Build a link to your iCalendar template.

    Install the free Download Content plugin

    In order to make your ExpressionEngine-generated .ics files be treated like a downloadable file, grab and install the free Download Content plugin.

    • Download the plugin from GitHub.
    • Unzip and upload the enclosed download_content folder to /system/user/addons.
    • Visit the Add-on Manager in your control panel, and click Install for the “Download Content” plugin.

    Create your download template

    Now we need to create an endpoint that we will link to that allows our visitors to download our .ics file. In this example, I’m creating an iCalendar file for a free concert the local brewpub is putting on. I have created a template named add-to-my-calendar inside my concerts template group.

    It uses the entry date and expiration date of the concert, the title, and a custom field called meta_description. Side note: meta_description is part of my standard set of SEO fields that I add to every channel. Repurposing it for the description of the calendar event makes sense. I’m also going to take advantage of a few iCalendar fields to setup a default alarm two hours before the event.

    {exp:channel:entries channel='concerts' limit='1' show_future_entries='yes' require_entry='yes'}
    	{if no_results}
    		{redirect='404'}
    	{/if}
    
    {exp:download_content filename='{url_title}.ics'}
    BEGIN:VCALENDAR
    VERSION:2.0
    BEGIN:VEVENT
    UID:downloadable-icalendar-files-{entry_date format='%Y%m%d'}@{site_name:url_slug}
    DTSTART;TZID={entry_date format='%e:%Y%m%dT%H%i%s'}
    DTEND;TZID={expiration_date format='%e:%Y%m%dT%H%i%s'}
    SUMMARY:{title}
    X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
    LOCATION:The BrewPub\n62950 NE 18th St\nBend\, OR 97701
    BEGIN:VALARM
    X-WR-ALARMUID:downloadable-icalendar-files-{entry_date format='%Y%m%d'}@{site_name:url_slug}
    TRIGGER:-PT120M
    DESCRIPTION:{meta_description}
    ACTION:DISPLAY
    END:VALARM
    END:VEVENT
    END:VCALENDAR
    {/exp:download_content}
    
    {/exp:channel:entries}

    Let’s break down the tags:

    {exp:channel:entries channel='concerts' show_future_entries='yes' limit='1' require_entry='yes'}
    
    • show_future_entries= lets us use the Entry Date as the start time of the event. Since we want people to see events that haven’t happened yet, we tell the Channel Entries tag to show future events.
    • limit='1' require_entry='yes' is a common design pattern for single-entry templates. This makes sure we only show one entry, and we don’t want it to display anything if the URL title is invalid. We combine it with:
    {if no_results}
        {redirect='404'}
    {/if}
    

    If there are no results—an invalid URL, an entry that’s already expired, a closed status, etc.—we will have proper 404 behavior.

    The next tag is the Download Content plugin we installed earlier.

    {exp:download_content filename='{url_title}.ics'}
    

    Using the entry’s URL title for the filename isn’t a requirement, but it will help the visitor identify the file’s purpose after downloading. It needs an .ics extension so desktop and mobile calendar apps know that the file is for them. This plugin makes sure the output only contains what’s inside the plugin’s tag, and gets treated as a download instead of displaying it on the screen.

    iCalendar files are picky about whitespace. This is why the code is not indented like we would normally encourage.

    The rest of the contents are made from standard iCalendar file format attributes. You can read more about that file format and its attributes here: iCalendar file format. Here are some notes to help dissect our template decisions:

    • UID: Providing a UID allows the calendar app to change the existing event if it is updated. Otherwise adding a new version will create a new event. The URL title, date, and site name should give us a unique ID. The :url_slug variable modifier replaces spaces with slashes and so forth so we can use the site’s name here.
    • DTSTART/DTEND: The format parameter used here (%e:%Y%m%dT%H%i%s) will output: America/Los_Angeles:20180827T105300
    • We use the entry_date for the start date and the expiration_date for the end date.
    • SUMMARY uses the entry’s title for the event’s name
    • Some calendar apps may not use the DESCRIPTION, but we add it for those that do. Likewise, we add DTSTAMP because Outlook demands it.
    • TRIGGER:-PT120M sets an alarm for 120 minutes (2 hours) before the event
    • X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC sets an alarm for Apple clients when the person needs to leave their current location to make it to the event on time.

    Create a download link

    Now all we need to do is link to our .ics file and we’re good to go! From my concert listing page, I’ll add:

    <a href="{url_title_path='concerts/add-to-my-calendar'}">Add this concert to my calendar!</a>
    

    Now when I click the link, it downloads a properly formatted .ics file that I can add to my calendar!

    With this knowledge you can create events, todos, journal entries, and any other kind of iCalendar file you can imagine. Use these same principles to output your content in other file formats, too; the sky’s the limit! With ExpressionEngine, you never have to work hard to make your content work hard for you.

    | Read in 5 minutes
  • Getting Started: A 10-Minute Primer

    in: Tutorials, Templating

    Get started using ExpressionEngine FAST. This primer will introduce you to the basic concepts needed to master ExpressionEngine, and build a fully functional blog in just 10 minutes!

    Learn the basics of building a beautiful web site with ExpressionEngine and make a fully functional blog from scratch in just 10 minutes!

    Subscribe to future videos here: http://bit.ly/ExpressionEngineTV

    Music: https://www.bensound.com

    Transcript

    ExpressionEngine is one of the web’s most flexible content management systems.You can create, really, any kind of website with it. But we wanted to make a quick primer for those new to ExpressionEngine to show how all the pieces work together; how you put in content, how you display that content.

    And so, we’re just going to make a simple blog for now, it’s a type of site that we’re all familiar with so it should make things easy to understand. So, I’ve got a fresh install ready here, a blank canvas to create our site, and I’ve logged into the control panel.

    The first thing I like to do is name my site, and there’s this button up here that’s kind of calling out to me to do that, so we’ll click through and we’ll just call it, “My Blog.” And we’ll update the short name as well, and we’ll click Save. And you can see the new name shows up right here in the top left of our control panel.

    Now from here, we need to set up a way to enter our blog’s content, and to do that in ExpressionEngine, we need to setup a Channel. Now, Channels are basically the different types of content your site has, and your content lives in these Channels. Maybe think of them as the buckets for your different types of content.

    So, if your site has events, you might have an events Channel, or if your site has recipes, you might have a recipes Channel. But the important thing with Channels is that allow you to break free of the idea of content living on a certain page on your site, we don’t really have to think about that now, we just want our content to be able to stand on its own, and then ExpressionEngine will allow us to display it on any part of the site that we want and within any design that we want.

    So for our blog, we just need to make one simple Channel. And to do that, we’ll go the Developer menu and click Channels, and then Add New. And we’ll just call it, “Blog.”

    Next we want to add the fields, or the different parts of our content that we want to capture separately. So again, if you had an events Channel, you might use some Date fields to capture some dates, or your recipes Channel might use a Grid field to capture all the steps and ingredients that appear in a recipe.

    But for our blog, we at least want to capture the blog entry text itself, and for that, I just want to use a simple Textarea, we’ll call it, “Body.” And I want to choose Markdown as the formatting type because I want to keep my content as clean and as portable as I can, and you can install this formatting type via the Add-ons page if the option isn’t showing up here for you. And with that, we’ll click Save, and you can see it shows up here.

    But there’s a couple more fields I think I might want, one is an excerpt field, in case I want more control over the excerpt that appears on my blog homepage. So we’ll create that the same, maybe make it a little smaller.

    And I also think I want a featured image be a part of my blog posts. and for that I want to use a File field so I can upload a file directly from the publish form. We’ll call it, “Featured Image.” And to use the file field you do need to have set up an Upload Destination in ExpressionEngine which I must confess I did set up prior to this so I didn’t have to break the flow, but it’s a simple thing to do. And with that, we’ll save our Channel.

    And you’ll see it now appears under the Create menu, and if we click through, you’ll see all the fields that we just created. So this is ready for me to post my first blog entry. I’ll just paste in some placeholder content here.

    So we have our first entry, but how do we display this on our website? For that, we need to write some templates. So we’ll go to the Template Manager, and first need to make a Template Group for our templates to live in. So we’ll just call it blog, and we’ll keep it the default group, and we’ll click Save. And when we make a Template Group, it automatically creates an index template for us, and since this Template Group is our default Template Group, this index template is actually our site’s homepage.

    Now ExpressionEngine lets us edit the template within the control panel here, but when you create a template, it also creates a file that you can edit with a text editor, and many folks prefer to just work with that file directly. So I’m going to do that. So here’s our installation, I’m going to go to system, user, templates and here is that same site short name that we set up earlier, so our templates must be in here. And here is that same index template that we just created under our blog Template Group.

    Now one of the things that makes ExpressionEngine stand out is its easy, clean, but powerful template language. If you know HTML, it should look pretty familiar and be easy to understand. For example, say you wanted to display the site name that we set up earlier. Instead of angle brackets like in HTML, ExpressionEngine’s template tags and variables use curly braces. And of course you can write any HTML you like around these variables to make them look how you want.

    So we’ll save this, and now let’s take a look at our homepage. And there is the site name that we set up earlier surrounded by those H1 tags. But let’s start writing the code that shows us our blog entries. I’m going to start out with an opening curly brace, and type exp:channel, because our blog content is in a channel, and we want the entries from that channel.

    Now here’s where the HTML knowledge is going to come in handy because we’re going to add some attributes, or parameters, to this tag. First we need to tell it which channel we want to pull from because we can have all kinds of channels, we want to pull from the “blog” channel. And since this is our homepage, we want to show the newest entries first so we want to order by the entry date and we want to sort descending. And let’s say we only want to show the most recent 10 entries. And again like in HTML, we’re going to write a closing tag.

    Now in between these two tags is where we’re going specify how we want our content displayed. And whatever is inside the tags here is going to repeat for each blog entry. So let’s say we want to show the entry title inside some H2 tags here. And then right below that, we want to show our excerpt. So we’ll save that and let’s go see how it looks.

    And there’s our blog entries. Here’s the one I added earlier, and then a couple more that I went back and added just to fill out the listing some more. But, it looks like this one is missing an excerpt. Maybe I forgot or just didn’t feel the need to make a special excerpt for that entry. But I still want to show a little preview of the entry so I’m going to go back to my template and add a conditional.

    And if you’re at all familiar with programming, this should make some sense. We’re basically going to say {if excerpt}, basically if the excerpt exists, show the excerpt, otherwise, take the body of the blog article and limit it to, let’s say, 200 characters. And we’ll close the conditional. And we’ll refresh.

    Ah, that’s looks better. Now we’re missing one crucial thing and that’s to be able to read the full article. So, to do this, we need to make a new template. And I actually don’t need to go to the control panel to make this template, I can just make a new file inside our Template Group. So we’ll just call it, “article.” And in that template, we want to put in a Channel Entries tag like on our homepage except this Channel Entries tag is going to let the URL tell it which entry to show, and we want to make sure it shows nothing else, so we want to put this require_entry= parameter on there.

    And we’ll change this to show the article body. And next, we’ll go back to our homepage template, and add a link to view the full article. We’ll just make a link out of the title here, and we can use this handy url_title_path variable, and we’ll point it to our article template that we just set up in our blog Template Group. And what this variable will do is automatically generate a URL based on the entry’s title.

    Ok, so let’s see if this works. We have links, and we’ll click through, and hey, there’s our full article. And if we take a look at the URL up here, you can see it’s loading the article template, and it’s put a nice, readable title for this entry so that the article template knows which entry to show. And with that, our blog is pretty much finished.

    But there’s one glaring thing we haven’t addressed and it’s that this doesn’t look very good, we’re just using the default browser styles here. And that’s because, if we look at the source, ExpressionEngine doesn’t inject its own markup or CSS, you are 100% in control of that. So to fix this, I’ve found this free template that I kind of like from a template site, and it’s just the HTML, it wasn’t even built with ExpressionEngine in mind, but that’s ok, I can just take these templates, the homepage and article template here, and paste them into the templates we thet we just made, and then we just replace the static, hard-coded content with our dynamic ExpressionEngine tags. So, let’s do that.

    And that should do it. You might notice there’s a lot of duplicate code across the templates and you’ll typically want put that stuff into a Template Layout, but that’s for another video. So, let’s take a look.

    And there you go, there’s my blog entries, and you can even see I got to use the featured images that I added to the entries, I just took the custom field variable and put it in the image tag source, and it just worked!

    So hopefully by now you get the gist of how ExpressionEngine works, and you have a good jumping off point for being able to publish your universe. Now there’s a lot more to cover from Template Layouts as I mentioned, to Fluid content, relationships, custom add-on development, so much more. It’s all in our documentation, but also subscribe to this channel and we’ll try to break down these things together.

    | Read in 11 minutes
  • Using Gmail SMTP to Send Emails from your Website

    in: Tutorials, Configuration

    Gmail’s SMTP server requirements differ from ExpressionEngine’s defaults. Google also has a few extra steps on their end to get up and running. Here’s how.

    Create an App Password for your Google Account

    If you have enabled 2-Step Verification on your Google account—which you have, right?—you will need to set up an app password that ExpressionEngine can use to authenticate. In Google, visit My Account, Sign-in & security, and under the Signing in to Google section, visit “App passwords”. Here is a direct link: https://myaccount.google.com/apppasswords.

    On the following screen, choose “Other” under “Select app”, and type in the name of your website or your domain. This will help you remember what you generated this app password for.

    Click Generate. Google will then give you the app password that you will use in ExpressionEngine. You need to copy this password to your clipboard or keep the browser window open. When you click Done, there is no way to retrieve this app password. Don’t worry if you jumped the gun and clicked Done before copying though; just revoke the app password and make another one.

    The generated app password. Copy this to the clipboard before closing the window.
    The generated app password. Copy this to the clipboard before closing the window.

    If you do not have 2-Step Verification enabled, and for some reason do not want to enable it, you will have to enable less secure sign-ins under Connected apps & sites. Toggle the switch under “Allow less secure apps” to “ON”.

    Configure Outgoing Email Settings in ExpressionEngine

    In ExpressionEngine, navigate to Settings > Outgoing Email. Change the Protocol to SMTP which will open the SMTP Options. Use the following settings:

    Server address
    smtp.gmail.com
    Server port
    465
    Username
    your.email@gmail.com
    Password
    The app password you generated earlier
    Connection type
    SSL (ssl://)

    Lastly make sure your Newline character setting on this page is set to \r\n, a requirement for Google’s SMTP servers.

    That’s it!

    You are finished and ready to start sending emails through your Gmail account. In the Developer menu, visit Utilities, and from the Communicate utility, send yourself a test email to verify that everything is working. If you get an error message, go back through the steps starting with your Google My Account page, and verify your settings.

    Caveats

    Using Gmail for website-generated emails is less than ideal. There is a sending limit of 100 emails per day via SMTP (G Suite accounts with a custom domain don’t suffer this limitation). But marketing and even transactional emails coming from a Gmail account may get flagged by email servers as spam, or even as phishing emails. This affects both the deliverability of your emails as well as your account reputation. Google has a laundry list of bulk sender guidelines, and problems might result in your Gmail account being suspended.

    So as a general rule, we do not recommend using Gmail SMTP for your website’s emails. It can be okay for your development environment, or if you just need something better than PHP mail() or sendmail for admin notifications, password reset emails, etc., and have only a handful of users. If your site has an email contact form, member registration, commenting, or discussion forums, you should use a transactional email service like:

    Marketing emails are a different category from transactional emails, which are the type triggered by a user taking an action. Registering, resetting a password, being notified of subscribed comments, and so on. For marketing and bulk email, you should use a service made for that purpose like Campaign Monitor, ConstantContact, or MailChimp. Some of the transactional providers also handle marketing emails, and mention that specifically in their marketing.

    If you are running a version of ExpressionEngine older than 3.5.0, this article still applies but you will need to set the newline and connection type manually in your system/user/config.php file.

    | Read in 4 minutes
  • Cross-Origin Resource Sharing (CORS)

    in: Tutorials

    If you’re seeing any of these things, there’s a good chance that you are experiencing a cross-origin resource failure:

    • Font-based icons (or any web font) not showing up
    • JavaScript failing in your control panel with add-ons while using the Site Manager
    • Front-end Ajax endpoint that won’t load

    In short, web browsers don’t allow sites to make requests to origins that differ from the one it is accessing. Developers are most affected by this restriction with web fonts and Ajax requests. The “origin” includes both the protocol (http://, https://) and the domain. It may not be immediately obvious that all these are different origins:

    • http://example.com
    • https://example.com
    • http://www.example.com
    • http://subdomain.example.com

    How do you know that you’re experiencing a problem with cross-origin resource sharing? If you open your developer tools in your browser, the console will have logged an error. The wording differs based on the browser, but typically will mention “Access-Control-Allow-Origin” somewhere in the message.

    Notice in the above examples that a domain with and without www are two separate origins. If this is the reason you’re experiencing a CORS issue, it is best solved by using a simple canonical redirect to either remove or force www in your URLs. This is best practice even if you aren’t having an issue with cross-origin resource sharing.

    Depending on the scenario you are facing, we will use a slightly different solution. In every case, we are adding an appropriate Access-Control-Allow-Origin header to the request. We use * if we don’t care about the requesting domain, or a specific origin if we need tighter control.

    Web Fonts Won’t Load

    Let’s say you have a @font-face declaration in your CSS, but the font is on a different origin. In that case, we will whitelist cross-origin access just for font files. In the folder where the font files live, add a .htaccess file with the following:

    # Allow font assets to be used across domains and subdomains
    <FilesMatch "\.(ttf|otf|eot|woff)$">
      <IfModule mod_headers.c>
        Header set Access-Control-Allow-Origin "*"
      </IfModule>
    </FilesMatch>

    JavaScript Errors in the Control Panel

    If you’re getting JavaScript errors in your ExpressionEngine control panel, and upon inspection see that something is not loaded due to cross-origin policy, you’ll need to add a header covering those resources. In this scenario:

    • An addon named Example is attempting to make an Ajax request to the themes folder
    • The control panel runs from https://admin.example.com
    • The Themes URL runs from https://example.com

    In the user/themes/example/ folder add a .htaccess file with the following:

    # Allow access to these theme files from https://admin.example.com
    <IfModule mod_headers.c>
      Header set Access-Control-Allow-Origin "https://admin.example.com"
    </IfModule>

    Note that we don’t have to specify https://example.com, because browsers will always allow access to resources from the same origin they live on.

    Front-end Ajax Endpoint Won’t Load

    In this scenario, you have a template that you’ve set up to act as an Ajax endpoint. We’ll assume it’s a public resource, so we will allow * origins.

    We have two different ways to do this. One is with server config, like the .htaccess we’ve used so far, and the other is to control it within your ExpressionEngine template itself. In both cases, we’re assuming a URL of https://example.com/ajax/endpoint.

    Using .htaccess

    In the web root’s .htaccess, add:

    # Allow cross-domain access to our Ajax endpoint
    <IfModule mod_headers.c>
      SetEnvIf Request_URI "/ajax/endpoint" CORS=True
      Header set Access-Control-Allow-Origin "*" env=CORS
    </IfModule>

    The first line sets an environment variable named CORS, but only for our specific URI. The second line sets the Access-Control-Allow-Origin header as normal, but the addition of env=CORS means that it will only set the header when that environment variable is set.

    This method is effective whether ExpressionEngine manages the resource, and allows regular expression URL patterns much as you’d use RewriteCond %{REQUEST_URI} with mod_rewrite.

    In an ExpressionEngine Template

    You may prefer for the cross-origin access to be set in the ExpressionEngine template itself, so another developer (including your future self) doesn’t have to hunt around to figure out how and where the header is set, or why. In this case, you can use a simple plugin to add cross-origin resource sharing to any template: Cross-Origin-Headers (plugin on GitHub).

    {exp:cross_origin_headers}

    This will allow * origins to access the content. Or to allow only a unique foreign origin:

    {exp:cross_origin_headers domain='https://subdomain.example.com'}

    Behind the scenes this extremely lightweight plugin sets the Access-Control-Allow-Origin header for us with PHP.

    Allowing Multiple Domain Origins

    One complication of the CORS implementation is that it only allows a resource to either be made fully public, or allowed to a single foreign origin. What if you have many domains (including subdomains) that need to share secure resources? You can accomplish this with server configuration, but it does have a caveat. In the folder that includes the resources that need to be shared, add the following .htaccess:

    # whitelist domains to allow CORS between, including http/https
    <IfModule mod_headers.c>
        SetEnvIf Origin "http(s)?://(example.com|subdomain.example.com)$" AccessControlAllowOrigin=$0
        Header set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
    </IfModule>

    Just replace example.com|subdomain.example.com in the above with a pipe-delimited list of the domains you need to allow. The first line sets an environment variable, only if the origin is in our list. The second line sets our header with the domain that we matched, only when the environment variable is set.

    What’s the caveat? Origin is a request header, and thus cannot be trusted. It works in all major browsers, so it is convenient and adds a modest amount of protection compared with *. But, a malicious user could violate your cross-origin policies by faking the Origin header. If you use this method on secure resources, make sure that they are adequately protected with server/user authentication, and do not rely soley on cross-origin browser policies.

    Further Reading

    If this topic is of interest to you, or you need to dive more deeply than the examples provided, here are some more resources for you.

    Browser Error Message Reference

    The following is a list of error messages from major browser vendors related to CORS. I’ve included it here to help you identify if that’s the issue you’re having, and to hopefully expose this article to people searching for help with these error messages.

    • Chrome / Opera
      • Access to Font at ‘https://subdomain.example.com/some-font.woff’ from origin ‘https://www.example.com’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://www.example.com’ is therefore not allowed access.
      • XMLHttpRequest cannot load https://subdomain.example.com/ajax.html. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://www.example.com’ is therefore not allowed access.
    • FireFox
      • Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://subdomain.example.com/ajax.html. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). (unknown)
      • Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://subdomain.example.com/some-font.woff. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
      • downloadable font: download failed (font-family: “Some Font” style:normal weight:500 stretch:normal src index:1): bad URI or cross-site access not allowed source: https://subdomain.example.com/some-font.woff
    • Safari
      • XMLHttpRequest cannot load https://subdomain.example.com/ajax.html. Origin https://www.example.com is not allowed by Access-Control-Allow-Origin.
    • Windows Edge
      • SEC7120: Origin https://www.example.com not found in Access-Control-Allow-Origin header. cors.html
      • SCRIPT7002: XMLHttpRequest: Network Error 0x80700013, Could not complete the operation due to error 80700013. cors.html
      • CSS3116: @font-face failed cross-origin request. No Access-Control-Allow-Origin header. some-font.woff
    | Read in 7 minutes
  • Template Routes for Single Entry / Permalink URLs

    in: Tips, Tutorials, Templating

    Single entry permalink URLs are common on the web. We’re going to apply it to a blog here, as that’s readily understood, but the pattern will work for everything else as well.

    By default ExpressionEngine’s URLs work like this https://example.com/blog/entry/url-title, where “blog” is a template group, and “entry” is a template, which is fine. But nicer would be something like https://example.com/blog/url-title, cause who needs that extra entry segment anyway? You could handle all of this in the blog template group’s index, but that’s inelegant, full of conditionals, hard to read, and hard to maintain.

    Template Routes give us an easy to create and maintain implementation. What you’ll need is the following. A blog template group with the templates index and single-entry.

    Inside of index you’ll put your blog listing, this will cover paginated archives, and categorized listings as well—URLs like https://example.com/blog/P15 and https://example.com/blog/category/cat-url-title.

    Index might look something like this:

    {exp:channel:entries channel='blog' limit='25'}
    	<div class="blog-entry">
    		<h2><a href="{route='blog/single-entry' url_title='template-routes-for-single-entry-permalink-urls'}">{title}</a></h2>
    		<p>{entry_date format='%n/%j/%Y'}</p>
    		{if has_categories}
    			<p><b>in</b>: {categories backspace='2'}<a href="{path='blog'}" title="View more in {category_name}">{category_name}</a>, {/categories}</p>
    		{/if}
    		<p>by: {author}</p>
    	</div>
    
    	{if no_results}
    		{redirect='404'}
    	{/if}
    {/exp:channel:entries}

    See that route= variable above? It’ll come into play in a moment. Next you’ll need a single-entry template that will cover your, well, single entries. And will be reached with URLs like https://example.com/blog/url-title.

    Single Entry will look something like this:

    {exp:channel:entries channel='blog' limit='1' require_entry='yes'}
    	<h1>{title}</h1>
    	<p>{entry_date format='%n/%j/%Y'}</p>
    	{if has_categories}
    		<p><b>in</b>: {categories backspace='2'}<a href="{path='blog'}" title="View more in {category_name}">{category_name}</a>, {/categories}</p>
    	{/if}
    	<p>by: {author}</p>
    
    	{blog_content}
    
    	{if no_results}
    		{redirect='404'}
    	{/if}
    {/exp:channel:entries}

    Now that you have those set up, you need to add your route. Go to Developer > Template Manager > Template Routes and set up a route for the template single-entry Give it a Route of /blog/{url_title:regex[(((?!(P\d+|category\/)).)+?)]} select “no” for “Segments Required?” then save. The regex may look a little complex, so let’s break it down. (((?!(P\d+|category\/)).)+?) uses a negative lookahead assertion, so the route does not match if the second URL segment is either pagination (P\d+) or category/. We want those URL patterns to still use the ExpressionEngine default routing to the blog/index template.

    Now in your templates replace any path variables to your single-entry URLs to use the route= variable in the first code sample. This will make sure that when clicked the visitor is sent to the right place!

    That’s it! Now go forth and clean up your ExpressionEngine URLs today!

    | Read in 3 minutes
.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases