Structured Data, Facebook Open Graph & Twitter Cards in Craft.


Phew, catchy title eh? Well I'm not sure there's a collective term for the social metadata/tags that are starting to clutter up the head of most websites these days. 'Clutter' is a little unfair because we're only talking about a few extra bytes, and the SEO and social benefits far outweigh the extra milliseconds it may add to your page speed.

I've already touched on the what, why and wherefore of this stuff in What is Structured Data & Why Should I Care? and I encourage you to delve more deeply into Structured Data/Schema, Open Graph (for Facebook), and Twitter Cards when you have time. In this article though I thought I'd share a practical example of how to add some of this metadata to your Craft site using a macro.

Custom fields

First off you'll need to create 3 custom fields you can use on entries:

Title (text input)

Obviously Craft includes a Title field with entries, but entry title and page title (title) are different things in my opinion, and so should be different fields. So I create an additional Title field I can use for title tags, and also in the aforementioned macro. I always fallback to entry title in my templates though if the page title is left blank.

Description (text input)

Description can be used for both your page meta description and in the macro we're about to create.

Put a character limit on both Title and Description fields so the client doesn't get too wordy here – 60 on Title and 160 on Description.

Image (asset)

Image is a single asset field that we can use for Structured Data, Facebook and Twitter.


Next, let's get started on the templates. In our master layout we need to add the following in our head:

    {% block head %}
    {% endblock %}

Now we can inject content into our document head from our entry template using the macro we're about to create. But first, here's how it might look in your entry template as you use the custom fields mentioned above to build your head.

{% block head %}
    {{ parent() }}
    	{{ craft.config.devMode ? 'DEV MODE - ' }}
        {{ entry.sdTitle ? entry.sdTitle : entry.title ~ ' | ' ~ siteName }}
    <meta name="description" content="{{ entry.sdDescription }}">
    {{ structuredData.structuredData(
        entry.sdTitle ? entry.sdTitle : entry.title,
        entry.sdDescription ? entry.sdDescription : '',
        entry.sdImage.first ? entry.sdImage.first : ''
    ) }}
{% endblock %}

Notice the {{ parent() }} tag, which tells Craft to append the following code to block head, not replace existing code in that block.

Then we're creating our title from a ternary operator that checks for our custom Title field value ('sdTitle') or falls back to the entry title. Oh, and I like to add 'DEV MODE' to the title too, so the client and I can easily distinguish between live and development sites.

We may as well use our Description field ('sdDescription') for the meta description on line 7. Then comes the macro, where we pass in values for URL, title, description and image.


Almost there. Now let's take a look at the macro that will output Facebook, Twitter and Schema data for our entry.

{% macro structuredData(url,title,description,image) %}
	{# Optimum image size for Open Graph/Facebook/Twitter #}
	{% set transform = { 
		format: 'jpg', 
		mode: 'crop', 
		position: 'center-center', 
		width: 1200, 
		height: 630 } 
	<!-- Facebook -->
	<meta property="og:url" content="{{ url }}">
	<meta property="og:site_name" content="{{ siteName }}">
	<meta property="og:type" content="website">
	<meta property="og:title" content="{{ title }}">
	<meta property="og:description" content="{{ description }}">
	<meta property="og:image" content="{{ image ? image.url(transform) : siteUrl ~ 'assets/images/fallback.jpg' }}">
	<meta property="og:image:type" content="image/jpg">
	<meta property="og:image:width" content="1200">
	<meta property="og:image:height" content="630">

	<!-- Twitter -->
	<meta name="twitter:card" content="summary_large_image">
	<meta name="twitter:site" content="{{ siteName }}">
	<meta name="twitter:creator" content="@yourtwitterhandle">
	<meta name="twitter:title" content="{{ title }}">
	<meta name="twitter:description" content="{{ description }}">
	<meta name="twitter:image" content="{{ image ? image.url(transform) : siteUrl ~ 'assets/images/fallback.jpg' }}">

	<!-- Structured Data -->
	<script type="application/ld+json">
		{"@context": "","@type": "WebSite","description": "{{ description }}","image": "{{ image ? image.url(transform) : siteUrl ~ 'assets/images/fallback.jpg' }}","url": "{{ url }}"}
{% endmacro %}

Okay, so at the top we're creating an image transform that will work for Facebook, Twitter and Schema. You could create individual transforms if you like, but I'm keeping it simple here.

Then it's just a case of outputting the values we passed in from our entry, with a fallback image if no image value was given.

Finally, we need to include the macro in our master template, so at the top of your master template add:

{% import "_macros/structured-data" as structuredData %}

Taking it further

This example is enough to add some basic metadata to your site to boost SERP, click-through, social sharing etc... but it's really just the start, especially when it comes to Structured Data/Schema. There are tons of attributes you can add that will increase SERP relevancy, and you should definitely take some time to read up at

Let us help tell your story.

Ready to inject some karma into your project?

Get started