ssg  
Article Series

Jekyll in General

Jekyll Plain

Where to Discuss?

Local Group

Preface

Goal: Flexible SEO with w3c, opengraph and twitter.

Source Code

This article use tutor-05 theme. We will create it step by step.


1: Prepare

Content is not just for human that using web browser, but also for machine that read this page, such as search engine. You can enhanced your SEO, using metadata already provided in frontmatter, such as tags and categories.

Default Plugin

Jekyll has default plugin for SEO.

If you want SEO quickly, you can skip this article.

Social Media

Don’t you love a preview on your site? I mean, that you can customize differently, based on the content. SEO is a whole huge topic, and deserve their own article series. This article only cover the most common meta tag for your content.

Jekyll Meta: Telegram Desktop Opengraph

You can see at above figure, each post has different image.

Description

In most SEO, either W3C, opengraph, and twitter, can have optionally have description. So first we are going to find out how to make description. One thing in my mind is to create excerpt in frontmatter in each page, but we should have fallback, just in case we forget to write excerpt.

{% capture spaceless %}

  <!-- calculate description -->
  {% if page.excerpt %}
    {% assign description = page.excerpt | strip_html %}
  {% else %}
    {% assign description = site.description | strip %}
  {% endif %}

  {% assign description = description
                        | newline_to_br
                        | strip_newlines
                        | replace: '<br />', ' '
                        | strip_html
                        | strip
                        | truncatewords: 150 %}
{% endcapture %}

This description is optional.


2: Refactoring: Head Tag

My first attempt is simple, consider move the meta tag to different template.

Artefacts

There will be few artefacts changes. I refactor site/head.html into two partials:

  • meta/html.html,

  • meta/seo.html,

Skeleton

To avoid complicated code, we are going to use partial for meta tags.

  • [gitlab.com/…/_includes/head.html][tutor-vi-head]
  <!-- calculate description -->

  {% include meta/html.html %}
  <title>{{ (page.title | default: site.title) | escape }}</title>
  {% include meta/seo.html %}

Or in a complete manners.

  {% include meta/html.html %}
  <title>{{ (page.title | default: site.title) | escape }}</title>
  {% include meta/seo.html %}
  {% include meta/opengraph.html %}
  {% include meta/twitter.html %}

We are going to use the first above first, to avoid complicated code.

SEO: HTML Preview

Don’t you love that your site is ready for Search Engine Optimization ? The HTML that we want to achieve is similar as example page below:

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible"
        content="IE=edge,chrome=1">
  <meta name="viewport"
        content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <meta name="HandheldFriendly" content="True">
  <meta name="MobileOptimized" content="320">

  <meta name="generator" content="Karimata 1.2.3 Media Engine" />
  <meta name="theme-color" content="#2980b9">

  <title>Julien Baker - Something</title>

  <meta name="author"
        content="Julien Baker">

  <meta name="keywords"
        content="lyric, rock, 2010s, sad, emo, broken">
</head>

You can see them from either page info.

Jekyll SEO: Meta View Source

Or from view source page.


3: Meta HTML

Consider move important meta to special _includes/meta/html.html. This should be an easy task, since it is only HTML.

Partial: Liquid: Meta HTML

Remember that, some of these metas must come first in the head tag.

  {% comment %}<!-- Required meta tags -->{% endcomment %}
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible"
        content="IE=edge,chrome=1">
  <meta name="viewport"
        content="width=device-width, initial-scale=1, shrink-to-fit=no">
  {% comment %}
  The above 3 meta tags *must* come first in the head;
  any other head content must come *after* these tags  -->
  {% endcomment %}

You can also add any metadata that you need, such as these lines below:

  <meta name="HandheldFriendly" content="True">
  <meta name="MobileOptimized" content="320">

  {% comment %} Let's Fake the Generator  -->{% endcomment %}
  <meta name="generator" content="Karimata 1.2.3 Media Engine" />

  {% comment %} Chrome, Firefox OS and Opera  -->{% endcomment %}
  <meta name="theme-color" content="#2980b9">

4: SEO: W3C

The next step is, put valid w3c SEO related meta.

We have at least these three meta tags:

  • Description

  • Keywords

  • Author

Keywords

The easiest thing to make keywords is to utilize built intags and categories in frontmatter, and additionally a keywords field, also in frontmatter. For example in a page we can set the frontmatter as below:

---
layout      : post
title       : Julien Baker - Something
date        : 2018-09-07 07:35:05 +0700
categories  : lyric
tags        : [rock, 2010s]
keywords    : [sad, emo, broken]
author      : Julien Baker
---

This will result as below:

  <meta name="author"
        content="Julien Baker">
 <meta name="description"
        content="I can't think of anyone, anyone else. I won't think of anyone else.">
  <meta name="keywords"
        content="lyric, rock, 2010s, sad, emo, broken">

Jekyll SEO Keywords: Qutebrowser View Source

Partial Liquid: Meta SEO

{% capture spaceless %}
  <!-- keywords -->
  {% assign cats  = '' | split: '' %}
  {% assign tags  = '' | split: '' %}
  {% assign words = '' | split: '' %}
  {% assign terms = '' | split: '' %}

  {% if page.categories  %}
    {% assign cats = page.categories %}
  {% endif %}
  
  {% if page.tags  %}
    {% assign tags = page.tags %}
  {% endif %}

  {% if page.keywords  %}
    {% assign words = page.keywords %}
  {% endif %}
  
  {% assign terms = cats | concat: tags  | concat: words %}
  {% assign termssize = terms | size %}
{% endcapture %}

  <meta name="author"
        content="{{ page.author | default: "epsi" }}">
  <meta name="description"
        content="{{ description }}">
{% if termssize != 0 %}
  <meta name="keywords"
        content="{{ terms | join: ", " }}">
{% endif %}

5: Plugin: Custom Filter

We can utilize liquid filter plugin, to calculate keywords. Just be aware that since not every CI/CD allow ruby plugin, we will not use this ruby plugin primarily.

Ruby Filter: TermKeywords

In ruby, the code is far shorter.

module Jekyll
  module TermKeywords
    def term_keywords(cats, tags, keywords)
      [cats, tags, keywords].reject{ |array| array.nil? }.flatten
    end
  end
end

Liquid::Template.register_filter(Jekyll::TermKeywords)

Using Filter

Here comes the good parts, using the filter in partials.

{% capture spaceless %} 
  {% assign terms = page.categories
                  | term_keywords: page.tags
                                 , page.keywords %}
  {% assign terms_size = terms | size %}
{% endcapture %}

  <meta name="author"
        content="{{ page.author | default: "epsi" }}">
{% if terms_size != 0 %}
  <meta name="keywords"
        content="{{ terms | join: ", " }}">
{% endif %}

6: SEO: Opengraph

The Structure of opengraph can be read from official page:

HTML Preview

The HTML that we want to achieve is similar as below.

  <meta property="og:locale"       content="en_US">
  <meta property="og:type"         content="article">
  <meta property="og:title"        content="Julien Baker - Something">
  <meta property="og:description"  content="I can't think of anyone, anyone else. I won't think of anyone else.">
  <meta property="og:url"          content="http://localhost:4000/lyric/2018/09/07/julien-baker-something.html">
  <meta property="og:site_name"    content="Your mission. Good Luck!">
  <meta property="og:image"        content="https://t2.genius.com/unsafe/220x220/https%3A%2F%2Fimages.genius.com%2Fad35fd5b6e1e7808990ebcc93e8179b9.1000x1000x1.jpg">
  <meta property="og:latitude"     content="-6.193665"/>
  <meta property="og:longitude"    content="106.848558"/>
  <meta property="og:locality"     content="Jakarta"/>
  <meta property="og:country-name" content="Indonesia"/>

I respect copyright, so instead of copy paste relevant image related to lyrics to my site, I grab original image URL from <genius.com>.

Structure

How to achieve ?

There are three parts:

  • Common properties: using conditional.

  • Image: using default image, depend on page type.

  • Location: set in theme configuration

Configuration: Opengraph Image

I put the default opengraph image in configuration.

#opengraph
locale: en_US
logo: /assets/images/logo-gear-opengraph.png

So if you forget to put opengraph image, in both page frontmatter and layout frontmatter, the page will use default image.

Layout: Post

So is the layout post

---
layout: default
aside_message : This is a post kind layout.

opengraph:
  image: /assets/images/authors/broken-person.png
---

So if you forget to put opengraph image in page frontmatter, the page will use default image in post.

Liquid: Opengraph Image

To make life easier, consider separate code and view. Javascript in code, and html in view.

This would takes some conditional, but it self explanatory. If there is no frontmatter setting, then use sitewide configuration setting. If neither set, then fallback to default image.

{% capture spaceless %}
  <!-- image -->
  {% if page.opengraph.image %}
    {% assign ogimage = page.opengraph.image %}
  {% elsif layout.opengraph.image %}
    {% assign ogimage = layout.opengraph.image %}
  {% else %}
    {% assign ogimage = site.logo %}
  {% endif %}

  <!-- fix -->
  {% unless ogimage contains "http://" or ogimage contains "https://" %}
    {% assign ogimage = ogimage | prepend: site.baseurl | prepend: site.url %}
  {% endunless %}  
  {% assign ogtitle = (page.title | default: site.title) | escape %}
  {% assign ogurl   = page.url 
                    | replace:'index.html',''
                    | prepend: site.baseurl
                    | prepend: site.url %}

{% endcapture %}

Now we have three possibility

  • Set in frontmatter.

  • General Post Image: set in images/author/broken-person.png.

  • Fallback Image: hardcoded as images/logo-gear-opengraph.png.

You can use any image, whatever you like, rather than just hardcoded example.

HTML: Opengraph Image

Now we can have tidy HTML right below, that above liquid code.

  {% comment %}<!-- Open Graph -->{% endcomment %}
  <meta property="og:locale"       content="{{ site.locale }}">
  <meta property="og:type"         content="article">
  <meta property="og:title"        content="{{ ogtitle }}">
  <meta property="og:description"  content="{{ description }}">
  <meta property="og:url"          content="{{ ogurl }}">
  <meta property="og:site_name"    content="{{ site.title }}">
  <meta property="og:image"        content="{{ ogimage }}">
  <meta property="og:latitude"     content="-6.193665"/>
  <meta property="og:longitude"    content="106.848558"/>
  <meta property="og:locality"     content="Jakarta"/>
  <meta property="og:country-name" content="Indonesia"/>

Page Content: Frontmatter Example

layout      : post
title       : Julien Baker - Something
date        : 2018-09-07 07:35:05 +0700
categories  : lyric
tags        : [rock, 2010s]
keywords    : [sad, emo, broken]
author      : Julien Baker

excerpt     :
  I can't think of anyone, anyone else.  
  I won't think of anyone else.

opengraph:
  image: https://t2.genius.com/unsafe/220x220/https%3A%2F%2Fimages.genius.com%2Fad35fd5b6e1e7808990ebcc93e8179b9.1000x1000x1.jpg

This will result as below:

  <meta property="og:locale"       content="en_US">
  <meta property="og:type"         content="article">
  <meta property="og:title"        content="Julien Baker - Something">
  <meta property="og:description"  content="I can't think of anyone, anyone else. I won't think of anyone else.">
  <meta property="og:url"          content="http://localhost:4000/lyric/2018/09/07/julien-baker-something.html">
  <meta property="og:site_name"    content="Your mission. Good Luck!">
  <meta property="og:image"        content="https://t2.genius.com/unsafe/220x220/https%3A%2F%2Fimages.genius.com%2Fad35fd5b6e1e7808990ebcc93e8179b9.1000x1000x1.jpg">

7: SEO: Twitter

The twitter version is another challenge.

Partial: Liquid: Meta Twitter

{% comment %}
<!-- Twitter Cards -->
{% endcomment %}

  <meta name="twitter:title"       content="{{ (page.title | default: site.title) | escape }}">
  <meta name="twitter:description" content="{{ description }}">
{% if site.owner.twitter %}
  <meta name="twitter:site"        content="@{{ site.owner.twitter }}">
{% endif %}
{% if author.twitter %}
  <meta name="twitter:creator"     content="@{{ author.twitter }}">
{% endif %}

{% if page.image.feature %}
  <meta name="twitter:card"        content="summary_large_image">
  <meta name="twitter:image"       content="{{ site.url }}{{ site.baseurl }}/images/{{ page.image.feature }}">
{% else %}
  {% if page.image.thumb %}
    {% assign twimg = site.baseurl | append: "/assets/images/" | append: page.image.thumb %}
  {% else %}
    {% assign twimg = site.baseurl | append: site.logo %}
  {% endif %}
  <meta name="twitter:card"        content="summary">
  <meta name="twitter:image"       content="{{ site.url }}{{ twimg }}">
{% endif %}

With result similar to below:

  <meta name="twitter:title"       content="Julien Baker - Something">
  <meta name="twitter:description" content="I can't think of anyone, anyone else. I won't think of anyone else."> 
  <meta name="twitter:card"        content="summary">
  <meta name="twitter:image"       content="http://localhost:4000/assets/images/logo-gear-opengraph.png">

I must admit, I do not really pay attention to twitter.


8: Preview: Social Media

How does it looks on social media? Enough with code, now see figure below.

Telegram on Smartphone

Jekyll Meta: Opengraph Telegram Smartphone

Slack on Smartphone

Jekyll Meta: Opengraph Slack Smartphone

The thing is, every social media interpret differently. In fact smartphone view and desktop, show have different result of opengraph image, based on image size and screen width, as you can see in inkscape article above

Beyond this Article

Just remember that SEO specification is changing from time to time, and this article is only covering the basic SEO. Here below is a reference for a serious SEO coder.

Good luck with SEO!


What is Next ?

Consider continue reading [ Jekyll - Plain - Pagination - Intro ].

Thank you for reading.