For pages we don't want to index, we can set ignore
to true
in the page's Front Matter Data. Here is an example:
<!-- src/root/404.njk -->
---
layout: base.layout.njk
ignore: true
---
Page not found. Go home!
At the minimum, we should set the title
tag and meta tag description
.
<!-- src/_includes/base.layout.njk -->
<title>{{ title }}</title>
<meta name="description" content="{{ desc or title }}">
In case there is no desc
provided in the template file, we will use the title
value as the description.
However, minimum is not good enough for social media sharing.
There are many social media meta tags you can set depending on how you want the data to display in the platforms. Read the specific social media documentation for the updated details.
I did the basic social media setup here:
1200 x 675 px
- each social media platform requires a specific image size for optimal display. You may consider generating different cover images for that. I aim for basic configuration. The trick is to always set the main content at the center-ish location area of the cover image, so when the image is cropped by social media, main content will still be seen. (Try sharing this post in social media and test if my theory works π)Here are the meta tags I used.
<!-- src/includes/base.layout.njk -->
<head>
<!-- Open graph -->
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ desc or title }}">
<meta property="og:type" content="article">
<meta property="og:image" content="{{ cover }}"/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="675" />
<!-- Twitter -->
<meta name="twitter:title" content="{{ title }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@yourUsername">
<meta name="twitter:description" content="{{ desc or title }}">
<meta name="twitter:image" content="{{ cover }}">
<meta name="twitter:creator" content="@yourUsername">
</head>
...
For open graph meta tags, we need to set the width and height to make sure the cover image displays correctly every time someone shares the link (see the issue and explanation here).
Check the documentation for og:type
and twitter:card
, and pick the type that is most suitable for your content. I use Twitter summary_large_image
because a big image looks great in a tweet.
Child templates will need to provide the cover
data - cover image URL.
Here is the example of all the Front Matter Data that a child template should have set.
<!-- src/root/index.njk -->
<!-- omit the layout if it's already set in root.11tydata.js -->
---
layout: base.layout.njk
title: Home Page
desc: This is my home page.
cover: /assets/img/cover-image.jpg
---
<strong>Hello Eleventy!</strong>
Deploy your project. Try sharing the page URL in social media now.
Oops, no cover image shows in the thumbnail.
This is because the cover image URL must be absolute (e.g. https://your-site.com/assets/img/cover-image.jpg). You might thought this is an easy fix - by adding the base image url to the cover
data like this:-
<!-- src/root/index.njk -->
<!-- Nice try, but this is not working! -->
---
title: Home Page
desc: This is my home page.
cover: "{{ env.base.img }}cover-image.jpg"
---
<strong>Hello Eleventy!</strong>
Nice try, but this is not working. π (Said this to myself)
Turns out, we cannot have global data access and string interpolation in Front Matter Data (except the special case permalink
, covered in previous post). That means your og:image
and twitter:image
are showing {{ env.base.img }}cover-image.jpg literally in the HTML.
To solve that, you can update the meta tag to include the base image URL.
<!-- src/includes/base.layout.njk -->
<head>
<!-- Open graph -->
...
<meta property="og:image" content="{{ env.base.img + cover }}"/>
<!-- Twitter -->
...
<meta name="twitter:image" content="{{ env.base.img + cover }}">
</head>
...
With that, you only need to type the shorter image name in every child template.
Let's face it, naming is hard. Typing the image URL manually in each template is time-wasting. It would be ideal if the cover
data is:
Here are the expected image names:-
src | assets/img |
---|---|
root/index.njk | root/index.jpg |
root/licenses.njk | root/licenses.jpg |
blog/2020-05-19-post-one.md | blog/post-one.jpg |
.
We can achieve this with the Global Computed Data eleventyComputed.js
. In fact, we used that in our previous post. We can apply similar techniques here.
// src/_data/eleventyComputed.js
const env = require('./env');
module.exports = {
// add this cover data
cover: (data) => {
let img = data.cover || (data.page.filePathStem + '.jpg');
img = img.startsWith('/') ? img.substr(1, img.length - 1) : img;
return new URL(img, env.base.img).href;
},
...
};
With the above code, we no longer need to enter cover
data in every template file. Sweet! π
You can test the code once it is deployed and publicly accessible. Various testing tools are offered by each platform - and each has its own fancy name...
So many different places to test! Yes. Once you set everything up correctly, then no more testing until the next time a social media platform decides to change their image display sizes... π
Sitemap is a good thing to have. You might not need one if you have linked your pages within your website. Crawlers are pretty good at discovering content automatically nowadays. Nevertheless, here is the code if you need to create one.
# src/root/sitemap.njk
---
layout: false
permalink: sitemap.txt
---
{%- for item in collections.all %}
{{ item.url }}
{%- endfor %}
Search engines accept sitemap in several formats. I am using a text file here, you may use xml. The collections.all
data is an Eleventy Supplied Data. It contains all the pages we created in our project. We loop through each one and write the URL in the sitemap.
Protip: Logging and debugging
How to know what are the properties available in collections.all
or item
? We can use the filter log
to examine the value. Here is how you can use it:
# src/root/sitemap.njk
{{ collections.all | log }}
Reload the page and check your dev console (where you run npm start
). The collections.all
data is logged.
Pretty handy! Use log
for debugging and discovery.
Browse to /sitemap.txt
, there are a few things we need to fix:
ignore
pages should be excluded - We marked 404
page as ignore: true
. We should exclude that from our sitemap..html
file extension - The pages will get indexed by Google and show up in the search result. It's okay actually, but I don't like that. π Let's remove that as well.Here is the fix:
# src/root/sitemap.njk
---
layout: false
permalink: sitemap.txt
---
{# {{ collections.all | log }} #}
{%- for item in collections.all %}
{% if not item.data.ignore %}
{{env.base.site}}{{ item.url | replace('.html', '') }}
{% endif %}
{%- endfor %}
Another way to fix this would be creating your own collection to filter out the unwanted pages, but we won't cover this for now. There is a lot to learn already!
Nice. Browse to /sitemap.txt
page again, you should see all URLs are absolute and cleaned! Go ahead and submit that to Google Search Console and probably the Bing Webmaster Tools too (which I did, because why not)!
Google has a detailed explanation on structured data. Adding structured data helps crawlers to understand your content more, and there would also be a possibility that your page would show up nicer in the search result.
We can define structured data with script format JSON-LD
. Both Google and Bing support that. We don't need structured data for every single page, only the main content (blog posts, presentation decks) will do, in my opinion (but I am not an SEO expert).
Again, we don't want to add the JSON-LD
script in every content page manually. One way to solve this is to do something similar to the ignore
. We can achieve this by using a new Front Matter Data, for example isSupportStructuredData
, to check the boolean value and toggle the script accordingly. However, let's not overuse the Front Matter.
Using a new layout would be a better option for our case. Let's create one and I will explain later why it is better.
# folder structure
- src
- _includes
- writing.layout.njk # new file
Update all our blog posts to use the writing layout. To save time, we can just add that once in the blog.11tydata.js
file.
// src/blog/blog.11tydata.js
module.exports = {
layout: 'writing.layout.njk',
...
};
The good thing about layout is - it is chainable. writing.layout.js
extends from the base layout (so we don't need to define those meta tags again π).
Alright, here is our writing layout with structured data.
<!-- src/_includes/writing.layout.njk -->
---
layout: base.layout.njk
---
<!-- the content -->
<main>
<article>{{ content | safe }}</article>
</main>
<!-- the structured data -->
{% set absoluteUrl = env.base.site + (page.url | replace('.html', '')) %}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"@id": "{{ absoluteUrl }}",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ absoluteUrl }}"
},
"url": "{{ absoluteUrl }}",
"headline": "{{ desc }}",
"description": "{{ title }}",
"audience": "web developers and designers",
"image": {
"@type": "ImageObject",
"url": "{{ cover }}",
"height": 675,
"width": 1200
},
"dateCreated": "{{ date }}",
"datePublished": "{{ date }}",
"dateModified": "{{ date }}",
"articleSection": "Blog",
"author": {
"@type": "Person",
"name": "{{ env.author }}",
"image": {
"@type": "ImageObject",
"url": "{{ env.base.img }}your_photo.jpg",
"height": 1024,
"width": 1024
},
"url": "{{ env.base.site }}"
},
"publisher": {
"@type": "Organization",
"@id": "{{ env.base.site }}",
"name": "{{ env.siteName }}",
"url": "{{ env.base.site }}",
"logo": {
"@type": "ImageObject",
"url": "{{ env.base.img }}your_photo.jpg",
"height": 1024,
"width": 1024
}
}
}
</script>
Structured data is lengthy. Luckily we just need to write it once in writing layout, not every post.
Use the writing layout in our blog post. Here is how our blog post look like:
<!-- src/blog/2020-05-19-post-one.md -->
---
title: A day of my life
desc: Story of a relaxing day.
date: 2020-05-20
---
I do nothing and sleep all day.
Layout
data is inherited from blog/blog.11tydata.js
and cover
image is defined in global data. We don't need to add those in the templates again.
View the script output in DevTools. Check if the data populated correctly.
Why is layout a better option in this case?
The writing layout does not only have structured content data, but it has some specific CSS for styling as well.
It is good for us to not mix it with base.layout.njk
. Keep the base layout clean.
You can validate the structured data with the Google structure data testing tool, even during development. Select the "code snippet" option and paste your JSON-LD
script there.
There is a handy feature in Chrome DevTools to help you to copy the script easily - in the Elements tab, right click on the script element > select Copy > Copy element.
There you go, the script is in your clipboard!
The fact is, I don't know either. I use DevTools to inspect the top article sites like Medium.com and Scotch.io to learn how they configured the structured data. That's how I came out with the above script. π
I also referred to the schema.org website to see what are the fields available for each entity.
SEO is a big topic. While I might not be an expert, here is a few things that can impact your site ranking:
Low hanging fruits:
Require more effort:
You can search for all your site URLs to check if your pages are indexed in search engine.
Type site:your_domain_url
in Google or Bing. You will see a list of your indexed pages. For example, this is the result when I google site:jec.fyi.
Alternatively, you can view it in Google Search Console and Bing Webmaster Tools.
Yay! We have learnt quite a bit in this post! From setting up Google Analytics, meta tags, and sitemap, to structured data, and various testing tools. Oh, we have also learnt about how to use layouts and global data!
All these efforts are mostly one time. Get it right and you won't need to worry about it anymore (until something suddenly breaks, heh π).
This is how I set up the SEO of my site jec.fyi as well.
In the coming posts, I plan to write about more on how I built my website with 11ty:
Let me know if the above topics interest you.
.
Here's the GitHub repo for the code above: jec-11ty-starter. I'll update the repo whenever I write a new post.
That's all. Happy coding!
Have something to say? Leave me comments on Twitter ππΌ
Follow my writing: @JecelynYeen