Template tags
{exp:metacraft:head}
Outputs everything that belongs in <head> for SEO, in one place:
<title>,<meta name="description">,<link rel="canonical">,<meta name="robots">- Open Graph:
og:title,og:description,og:url,og:type,og:site_name,og:image(+ width/height when known) - X:
twitter:cardandtwitter:sitealways.twitter:title,twitter:descriptionandtwitter:imageonly when the entry has an X-specific override; otherwise X reads the OG tags. - One
<script type="application/ld+json">block with a single@graph: WebSite, Organization (when named), WebPage or Article, and BreadcrumbList on Structure pages that have parent pages.
A tag is left out when there's no real value for it. MetaCraft never outputs an empty description or a placeholder image. The robots tag is also left out when it would say index, follow, because that's what search engines assume for a page without one. It appears only when something is restricted (noindex, nofollow) or extra directives are set.
Put it once, inside <head>, in whatever template, layout or partial builds your <head>:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{exp:metacraft:head}
<link rel="stylesheet" href="/css/site.css">
</head>
Remove any hard-coded <title>, description, canonical or og: tags from the same <head>. The Overview flags templates that still have them (Duplicate tags).
Which entry is the page about?
With no entry_id, MetaCraft works it out from the URL, in this order:
- Structure: the request URI matches a Structure page or listing entry. Matching ignores case and trailing slashes.
- Channel URL pattern: the URI matches an enabled channel's URL pattern (on the Sitemap screen), e.g.
/blog/{url_title}/. - Otherwise the page is a non-entry page: title = site name, canonical = the current URL without its query string.
A trailing pagination segment (/P20) is ignored when matching. On Structure and pattern pages it is added back to the canonical, so page 2 of a listing canonicalizes to itself, not to page 1.
Pass entry_id when the URL can't identify the entry, e.g. a template that shows an entry chosen some other way:
{exp:channel:entries channel="news" limit="1" disable="categories|member_data|pagination"}
{exp:metacraft:head entry_id="{entry_id}"}
{/exp:channel:entries}
(Calling the head tag inside channel:entries works, but the URL-based lookup above is cheaper when it applies.)
Parameters
All are optional. A parameter beats every other source.
| Parameter | Example | Effect |
|---|---|---|
entry_id |
entry_id="{entry_id}" |
Use this entry instead of detecting one from the URL. |
title |
title="Search results" |
Replaces {title} in the title format. |
title_exact |
title_exact="yes" |
Output the title as given, without the format (site name, separator). |
description |
description="…" |
Meta and OG description. |
canonical |
canonical="https://example.com/x/" or canonical="/x/" |
Canonical URL. canonical="none" outputs no canonical tag. |
robots |
robots="noindex, follow" |
Replaces the robots value entirely. |
image |
image="/images/share.jpg" |
Social image URL. |
og_type |
og_type="profile" |
og:type. Default article for Article entries, otherwise website. |
schema |
schema="none" |
none outputs no JSON-LD on this page. |
breadcrumbs |
breadcrumbs="no" |
No BreadcrumbList on this page. |
Parameter values are trusted template content. Entry and channel values are escaped. { and } from editors' text are encoded so ExpressionEngine never parses them as tags.
Common pages
Shared head in a layout (the usual setup). Child templates don't need to do anything:
{!-- layouts/_wrapper --}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
{exp:metacraft:head}
</head>
<body>{layout:contents}</body>
</html>
A page that needs a different value: pass it through a layout variable.
{!-- layouts/_wrapper --}
{if layout:seo_title}
{exp:metacraft:head title="{layout:seo_title}"}
{if:else}
{exp:metacraft:head}
{/if}
{!-- events/index --}
{layout="layouts/_wrapper" seo_title="Upcoming events"}
Shared head partial ({par_head} or an embed): same tag, same rules. The scanner follows partials, template variables, embeds and layouts.
Search results: keep them out of the index but let crawlers follow links:
{exp:metacraft:head title="Search" robots="noindex, follow"}
404 page: no index, no canonical:
{exp:metacraft:head title="Page not found" robots="noindex" canonical="none"}
Paginated listing (/blog/P10): nothing to do. If /blog is a Structure page, the canonical is /blog/P10/. Otherwise it's a non-entry page and the canonical is the current URL.
What the JSON-LD contains
One <script type="application/ld+json"> with an @graph:
- WebSite (always, when JSON-LD is on) and the site entity (when it has a name):
Organizationor the type chosen in Settings → Structured data, with its address, hours and medical details where they apply. They're linked by@id:{base}/#websiteand{base}/#organization. - The page, by its structured data type:
WebPage,MedicalWebPage,FAQPage,Article,BlogPosting: one node for the page.Event,Product,Service,Physician, location: aWebPagewhosemainEntityis the thing (#event,#product,#service,#physician,#locationon the page URL).Person: aProfilePagewhosemainEntityis the Person.- A required property without a value outputs a plain
WebPage.
- BreadcrumbList for Structure pages with ancestors.
Things link back to the site entity where schema.org expects it: publisher (articles), organizer (events), provider (services), parentOrganization (locations). Values never come from anywhere but your settings, entry fields and Grid rows, and empty properties are left out.
Duplicate tags
MetaCraft can't see other tags' output, so the Overview scans instead and lists templates that may output duplicate tags. It reads every template (database and files), template partial and template variable, follows {embed=}, {layout=} and {partial} references, and lists pages where {exp:metacraft:head} ends up next to {exp:seo_lite, a literal <title>, rel="canonical" or og:title.
- Duplicate: both were found.
- Possible: the page has the head tag but includes something the scanner couldn't follow, such as
{embed="{segment_1}/_x"}or a missing template. Check it by hand.
The scanner doesn't evaluate {if} conditions and ignores {!-- --} and <!-- --> comments. It never edits anything.
{exp:metacraft:sitemap}
Serves the XML sitemap from a template. The tag sends its own Content-Type: application/xml header and ends the request, so nothing else in the template (or any add-on's output filters) runs.
| Parameter | Example | Effect |
|---|---|---|
part |
part="{segment:part}" |
Serve child sitemap N when the site has more than 45,000 URLs (sitemap index mode). |
{!-- metacraft/sitemap (template type: XML, route: sitemap.xml) --}
{exp:metacraft:sitemap}
{!-- metacraft/sitemap_part (template type: XML, route: sitemap-{part:integer}.xml) --}
{exp:metacraft:sitemap part="{segment:part}"}
Responses: 200 with XML. 404 when the sitemap is turned off or the part doesn't exist. 503 with Retry-After only when there is no previous sitemap to serve while a rebuild runs.
Full setup, static files and cron: XML sitemaps.