So, ya built a site. Good job! It's a fancy pants ExpressionEngine website and totally purpose built to generate revenue and attention, too! Has all the features you'd need, puts all the information you want out in the universe, and gives all the right vibes and feely feels for visitors. Now it's time for the invisible layer: Search Engine Optimization (SEO).
Now, I'm not going into the "what" of SEO, so if you're unfamiliar, be sure to hit up the above link before diving deeper. I will be going into the how of SEO, especially in regards to ExpressionEngine and it's available tooling, without spending a lot of money. in the below I'm going to only focus on 3 components of SEO that every website should implement: meta content, schema, and a sitemap.xml document.
Quick note, I really didn't have any budget for this task, as it was for this *looks around* so didn't include any notes on cool tools like SEEO, Sitemap, or any of the other commercial Add-ons. This isn't to imply they're not worth it, just that with a little elbow grease, there isn't really much need to go crazy on paying for it when time is abundant.
Meta Content
For meta content, we're doing to use at least 1 free Add-on and/or the one plus a paid Add-on. The free Add-on is a wonderful little tool, that's been around for years and years, called SEO Lite Pro.
SEO Lite Pro
From the product page:
The most powerfull SEO module for ExpressionEngine. The add-on contains Meta, OG and Twitter tags. You can add default tags and add tags per page. With the audit page you can check which fields you’ve added and how it looks like on Google, Facebook and Twitter.
It's very easy to work with when publishing and pretty simple to implement as well; usually requiring a simple edit to the header.
Basic Example
It's a VERY simple tag pair that you wrap around your HTML meta content and use the variables to contain the exact data. And, yeah, you can set default values (through a couple places actually).
{exp:seo_lite entry_id="{embed:entry_id}"
default_title="mithra62 Custom Programming and Development"
default_keywords="php, ExpressionEngine, CraftCMS, development, programming"
}
<title>{og_title}</title>
<meta name="description" content="{meta_description}" />
<meta name="robots" content="{if robots_directive}{robots_directive}{if:else}INDEX, FOLLOW{/if}" />
<link rel="canonical" href="{current_url}"/>
<meta property="og:description" content="{og_description}" />
<meta property="og:type" content="{if og_type}{og_type}{if:else}website{/if}" />
<meta property="og:url" content="{current_url}" />
<meta property="og:image" content="{og_image}" />
<meta property="twitter:title" content="{twitter_title}" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:description" content="{twitter_description}" />
<meta property="twitter:image" content="{twitter_image}" />
{/exp:seo_lite}
That said though, for more advanced implementation, I usually abstract it using embeds. For example, on this site (at the time of writing), I have straightforward _embeds/_header
template that I pass the Channel Entry ID for use with SEO Lite to make my life easier on implementation. Be sure to check out the SEO Lite documentation for a full understanding of what's available. So far, we've only scratched the surface.
Carson
Now, the above is all well and good, but we live in the future, with AI tools and puppies and unicorns. We ain't got time to write meta content! sips margarita... and have $35...
This is where Carson, from Boldminded, comes in. From the official site:
Carson leverages AI to help you improve, change, or just find the right words for your content on the fly.
It does quite a bit actually, but for SEO purposes there's really only 1 feature we care about, the SEO FieldType. It's really neat! The Carson SEO FieldType will automatically generate your needed meta content based on the generated entry. Very very cool and time saving though, like all AI implementations, be sure to review the output to ensure it matches reality.
Schema (JSON)
This is where things get complicated. There really aren't any solid free tools available to generate Schema content automagically. There's SEEO, which I'm not familiar with though it does say it'll do schema. Basically, going manual and focused has always been my option with Schema JSON.
Now, this is a DEEEEEP topic and one way outside my intentions, so if you're unfamiliar be sure to review the Schema.org documentation. For my needs though, I either piggybacked on top of seo_lite
or channel:entries
tags and used that to generate most of my schema.
A useful tool to use when developing your schema JSON objects is the Rich Results Test (from Google), which will output what your search results will look for any given URL. Very handy and helpful.
If you know nothing else, know this: there are a TON of Types to use for Schema JSON. Lots and lots and lots. Be sure to checkout the documentation to get started. Below are just a couple examples and implementation examples.
Article Example
Below is what's used on this very template to output the schema for an article.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": "{title:attr_safe}",
"image": [
"{writing_header_image}"
],
"datePublished": "{entry_date format="%c"}",
"dateModified": "{edit_date format="%c"}",
"author": [{
"@type": "Person",
"name": "Eric Lamb",
"url": "https://github.com/mithra62"
}]
}
</script>
If you check the source of this page you'll see the executed output.
Documentation Schema
This is a more robust example that's on the documentation for projects here. Note that this includes quite a few subnodes and calls to channel entries tags to generate the schema. Beware your eyes...
{exp:seo_lite entry_id="{embed:doc_id}"
default_title="mithra62 Custom Programming and Development"
default_keywords="php, ExpressionEngine, CraftCMS, development, programming"
}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebPage",
"@id": "{current_url}",
"url": "{current_url}",
"name": "{og_title:attr_safe}",
"description": "{meta_description:attr_safe}",
"isPartOf": {
"@type": "WebSite",
"@id": "https://mithra62.com/"
},
"about": { "@id": "https://mithra62.com/projects/{embed:product_url_title}" }
},
{
"@type": "TechArticle",
"@id": "{current_url}",
"headline": "{og_title:attr_safe}",
"mainEntityOfPage": { "@id": "{current_url}" },
{exp:channel:entries channel="docs" limit="1" paginate="bottom" dynamic="no" entry_id="{embed:doc_id}"}
"datePublished": "{entry_date format="%Y-%m-%d"}",
"dateModified": "{edit_date format="%Y-%m-%d"}",
{/exp:channel:entries}
"proficiencyLevel": "Beginner",
"about": { "@id": "{current_url}" },
"author": {
"@type": "Organization",
"name": "mithra62"
},
"publisher": {
"@type": "Organization",
"name": "mithra62",
"logo": {
"@type": "ImageObject",
"url": "https://mithra62.com/assets/img/mithra-62-horizontal.png"
}
}
},
{
{exp:channel:entries channel="products" limit="1" paginate="bottom" dynamic="no" entry_id="{embed:product_id}"}
"@type": "Product",
"@id": "https://mithra62.com/projects/{url_title}",
"name": "{embed:product_title}",
"image": "{og_image}",
"description": "{elevator_pitch}",
"brand": { "@type": "Brand", "name": "mithra62" },
"category": "SoftwareApplication",
{releases limit="1" orderby="row_id" sort="desc"}
"softwareVersion": "v{releases:version}",
{/releases}
"offers": {
"@type": "Offer",
"price": "{price}",
"priceCurrency": "USD",
"url": "https://mithra62.com/projects/{url_title}"
}
{/exp:channel:entries}
}
]
}
</script>
{/exp:seo_lite}
All this said, a healthy knowledge of schema is a prerequisite.
Sitemap.xml
Thankfully, sitemap.xml files are easy to create and easy to work with. Just generate it, submit to Google, and you're off the races. With ExpressionEngine, like most CMS's, creating a sitemap.xml can be achieved through "just" creating one. With ExpressionEngine, that entails creating a new XML template within your site. For me, I just went with /sitemap.xml but, apparently, they can be at any location. I just like the cleanliness of having it at root.
The actual content is a VERY simple XML schema that just lists basic output of your site, like the URL, modified date, change frequency, and any priority. I'm not going to go into details on what all that means, but feel free to read the sitemap.xml documentation on Google.
<url>
<loc>URL_TO_CONTENT</loc>
<lastmod>DATE_IN format='%Y-%m-%dT%H:%i:%s+02:00'</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
Note that, per the documentation, Google ignores the changefreq
and priority
nodes, but they're good to keep for other search engines.
Basic Example
This is just to show how easy and simple implementation can be using regular ExpressionEngine template tags.
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
{exp:channel:entries channel="work" dynamic="no" status="open" orderby="edit_date" sort="desc"
disable="member_data|pagination|categories|custom_fields|relationships"}
<url>
<loc>{url_title_path="work"}</loc>
<lastmod>{edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{/exp:channel:entries}
</urlset>
As you can see, not much to it. Just a basic XML document that outputs everything within a given channel we can generate links to.
Complete Example
Now, the above is all well and good, but a single channel doesn't make a site. On this site, at the time of writing, we have multiple channels AND use Structure to handle the Documentation section. So, on the surface, it could get tricky, but nay nay. Easy as pie.
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
{exp:channel:entries channel="work" dynamic="no" status="open" orderby="edit_date" sort="desc"
disable="member_data|pagination|categories|custom_fields|relationships"}
<url>
<loc>{url_title_path="work"}</loc>
<lastmod>{edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{/exp:channel:entries}
{exp:channel:entries channel="writing" dynamic="no" status="open" orderby="edit_date" sort="desc"
disable="member_data|pagination|categories|custom_fields|relationships"}
<url>
<loc>{url_title_path="writing"}</loc>
<lastmod>{edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{/exp:channel:entries}
{exp:channel:entries channel="products" dynamic="no" status="open" orderby="edit_date" sort="desc"
disable="member_data|pagination|categories|custom_fields|relationships"}
<url>
<loc>{url_title_path="projects"}</loc>
<lastmod>{edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{/exp:channel:entries}
{exp:structure:nav_advanced start_from="/docs" show_depth="3"}
<url>
<loc>https://mithra62.com{root:page_url}</loc>
<lastmod>{root:edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{if root:has_children}
{root:children}
<url>
<loc>https://mithra62.com{child:page_url}</loc>
<lastmod>{child:edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{if child:has_children}
{child:children}
<url>
<loc>https://mithra62.com{grandchild:page_url}</loc>
<lastmod>{grandchild:edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{if grandchild:has_children}
{grandchild:children}
<url>
<loc>https://mithra62.com{great_grandchild:page_url}</loc>
<lastmod>{great_grandchild:edit_date format='%Y-%m-%dT%H:%i:%s+02:00'}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{/grandchild:children}
{/if}
{/child:children}
{/if}
{/root:children}
{/if}
{/exp:structure:nav_advanced}
</urlset>
Wrapping Up
May read like a lot, but it's really not (poetry!), just the same old paradigms we've had for years and years. Not much to it if you've done SEO before but, also, not much to learn if you haven't. Still, this does barely scratch the surface and just hits the low hanging fruit. Will still have to build and design things with search engines in mind, but the wholesale focus portions are done.
For now.