Preface

Content is not just for human that using web browser, but also for machine that read this page, such as search engine, or social media that read your opengraph.

Goal: Flexible SEO with Hexo, w3c, opengraph and twitter.

Don’t you love a preview on your site? I mean, that you can customize differently, based on the content.

Hexo Meta: Telegram Desktop Opengraph

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

Table of Content

  • Preface: Table of Content

  • 1: Prepare Artefacts

  • 2: Refactoring: Head Tag

  • 3: Meta HTML

  • 4: SEO: W3C

  • 5: SEO: Opengraph

  • 6: SEO: Twitter

  • 7: Preview: Social Media

  • What is Next ?

Source Code

You can download the source code of this article here.

Extract and run on CLI using $ npm install.


1: Prepare

There are few artefacts changes.

Artefacts

Edit

  • _config.yml.

  • themes/tutor-05/layout/site/head.ejs.

New

  • themes/tutor-05/layout/meta/html.ejs.

  • themes/tutor-05/layout/meta/seo.ejs.

  • themes/tutor-05/layout/meta/opengraph.ejs.

  • themes/tutor-05/layout/meta/twitter.ejs.

Example Frontmatter

  • source/_posts/lyrics/moody-blues-white-satin.md.

2: Refactoring: Head Tag

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

Skeleton

To avoid complicated code, we are going to us partial for meta tags. This including opengraph and twitter.

<%
  // embedded javascript code here
  ...
%>
<head>
  <%- partial('meta/html') %>
  ...

  <%- partial('meta/seo', {description: description}) %>  
  <%- partial('meta/opengraph', {title: title, description: description}) %>
  <%- partial('meta/twitter', {title: title, description: description}) %>
</head>

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">  

  <!--w3c-->
  <meta name="description" content="Letters I’ve written, never meaning to send">
  <meta name="keywords" content="lyric, pop, 60s, poetic, tragedy, truth, love">
  <meta name="author" content="Mataharani">  

  <!--opengraph-->
  <meta property="og:locale" content="en">
  <meta property="og:title" content="Moody Blues - Nights in White Satin">
  <meta property="og:type" content="article">
  <meta property="og:description" content="Letters I’ve written, never meaning to send">
  <meta property="og:url" content="http://example/2014/03/15/lyrics/moody-blues-white-satin/">
  <meta property="og:site_name" content="Heartbeats for Bulma">
  <meta property="og:image" content="http://example/site/images/adverts/one-page.png">
  <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">

  <!--twitter-->
  <meta name="twitter:title" content="Moody Blues - Nights in White Satin">
  <meta name="twitter:description" content="Letters I’ve written, never meaning to send">
...
</head>

Hexo Meta: SEO

You can also optimize twitter card if you want.

Javascript: EJS Head

Remember the javascript part, from last tutorial.

  var title = page.title;

  if (is_archive()){
    title = __('archive');

    if (is_month()){
      title += ': ' + page.year + '/' + page.month;
    } else if (is_year()){
      title += ': ' + page.year;
    }
  } else if (is_category()){
    title = __('category') + ': ' + page.category;
  } else if (is_tag()){
    title = __('tag') + ': ' + page.tag;
  }

This is just a reminder of the last javascript that we use__.__ We still need this javascript.

Javascript: Description

Have a look at partial calls, all three partial takes description as paramater.

  <%- partial('meta/seo', {description: description}) %>  
  <%- partial('meta/opengraph', {title: title, description: description}) %>
  <%- partial('meta/twitter', {title: title, description: description}) %> 

This description will be set by embedded javascript. Now add this embedded javascript.

  var description = '';

  if (page.description) {
    description = page.description;
  } else if (page.excerpt) {
    description = page.excerpt
                      .replace(/<[^>]*>?/gm, ' ')
                      .substring(0, 155)
                      .trim();
  }

We need to limit the length of the description, and alow remove trailing white space.


3: Meta HTML

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

Layout: EJS Head

<%
  ...
%>
<head>
  <%- partial('meta/html') %>  
  <title><%= title || config.title %></title>

  <% if (theme.favicon){ %>
     <link href="<%- theme.favicon %>" rel="shortcut icon" type="image/x-icon" />
  <% } %>

  <%- css(['/css/bulma.css', '/css/fontawesome.css', '/css/main.css']) %>
</head>

Notice this partial(‘meta/html’) code.

Layout: EJS Meta HTML

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

  <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">

Or better, we can add more meta html tag.

  <%/* Let's Fake the Generator */%>
  <meta name="generator" content="Karimata 1.2.3 Media Engine" />

  <%/* Chrome, Firefox OS and Opera */%>
  <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 do make keywords is to utilize built in tags and categories in frontmatter. For example in a page we can set the frontmatter as below:

tags      : [pop, 60s]
category  : [lyric]

This will result as below:

  <meta name="keywords" content="lyric, pop, 60s">

If these two are not enough we can add keywords field in frontmatter. So we can have more keywords without ruining, tags and categories in page. For example:

tags      : [pop, 60s]
category  : [lyric]
keywords  : [poetic, tragedy, truth, love]

These three frontmatter setting will result as below:

  <meta name="keywords" content="lyric, pop, 60s, poetic, tragedy, truth, love">

Layout: EJS Meta SEO

And here the powerful embedded javascript. It is very easy to do array manipulation using embedded javascript.

<%
  var keywords = '';

  var terms = [];

  if (page.categories) {
    page.categories.each(function(item){
      terms.push(item.name);
    })
  }

  if (page.tags) {
    page.tags.each(function(item){
      terms.push(item.name);
    })
  }

  if (page.keywords) {
    page.keywords.forEach(function(keyword){
      terms.push(keyword);
    })
  }

  if (terms) {
    keywords = terms.join(', ');
  }
%>

  <meta name="description"
        content="<%= description || config.description %>">
  <meta name="keywords"
        content="<%= keywords || config.keywords %>">
  <meta name="author"
        content="<%= page.author || config.author %>">

Just be aware, that the keywords is using forEach, while tags and categories using each. The differences is because keywords is simply array, while the other two processed internally by Hexo.

How Does Keywords Works?

Merge array into one string require join function.

  if (terms) {
    keywords = terms.join(', ');
  }

Content: Example

---
layout    : page
title     : Moody Blues - Nights in White Satin
date      : 2014/03/15 07:35:05
tags      : [pop, 60s]
category  : [lyric]
keywords  : [poetic, tragedy, truth, love]
---

Letters I've written, never meaning to send

<!-- more --> 

Nights in white satin, never reaching the end,  
Letters I've written, never meaning to send  
Beauty I've always missed, with these eyes before  
Just what the truth is, I can't say anymore  

'Cause I love you  
Yes, I love you  
Oh, I love you  

This will result as below:

  <meta name="description" content="Letters I’ve written, never meaning to send">
  <meta name="keywords" content="lyric, pop, 60s, poetic, tragedy, truth, love">
  <meta name="author" content="Mataharani">  

5: 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:title" content="Moody Blues - Nights in White Satin">
  <meta property="og:description" content="Letters I’ve written, never meaning to send">
  <meta property="og:url" content="http://example/2014/03/15/lyrics/moody-blues-white-satin/">
  <meta property="og:site_name" content="Heartbeats for Bulma">
  ...
  <meta property="og:image" content="http://example/site/images/adverts/one-page.png">
  ...
  <meta property="og:locality" content="Jakarta">
  ...

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

Javascript: 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.

  var ogi; 
  if (page.opengraphimage) {
    ogi = page.opengraphimage;
  } else if(config.opengraphimage) {
    ogi = config.opengraphimage;
  } else {
    ogi = 'images/broken-person.png';
  }
  ogi = config.url + url_for(ogi);

Now we have three possibility

  • Set in frontmatter.

  • General: set in _config.yml

  • Fallback: hardcoded as images/broken-person.png

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

Content: Frontmatter Example

title     : Moody Blues - Nights in White Satin
date      : 2014/03/15 07:35:05
opengraphimage: site/images/adverts/one-page.png

This will result as below:

  <meta property="og:image" content="http://example/site/images/adverts/one-page.png">

Config: YAML Example

We can set parameters in configuration, as below:

# Custom
opengraphimage : "images/logo-gear-opengraph.png"

Theme Configuration

To avoid hardcoded layout, theme can also have its own configuration as shown below:

# Opengraph

og_latitude     : "-6.193665"
og_longitude    : "106.848558"
og_locality     : Jakarta
og_country_name : Indonesia

The value in this theme configuration can be called in template.

Layout: EJS Meta Opengraph

Now the view part.

<%  
  ...
%>

  <meta property="og:locale"
        content="<%= config.language || "en" %>">
  <meta property="og:title"
        content="<%= title || config.title %>">
<% if (is_post()) {%>
  <meta property="og:type"
        content="article">
<% } %>
<% if (page.excerpt) {%>
  <meta property="og:description"  
        content="<%= description || config.description %>">
<% } %>
  <meta property="og:url"          content="<%= page.permalink %>">
  <meta property="og:site_name"    content="<%= config.title %>">
  <meta property="og:image"        content="<%= ogi %>">
  <meta property="og:latitude"     content="<%= theme.og_latitude %>"/>
  <meta property="og:longitude"    content="<%= theme.og_longitude %>"/>
  <meta property="og:locality"     content="<%= theme.og_locality %>"/>
  <meta property="og:country-name" content="<%= theme.og_country_name %>"/>

6: SEO: Twitter

The twitter version is much more simple. Because, I do not really pay attention to twitter.

  <meta name="twitter:title"
        content="<%= title || config.title %>">

<% if (page.excerpt) {%>
  <meta name="twitter:description"
        content="<%= description || config.description %>">
<% } %>

<% if (site.data.owner) {%>
  <meta name="twitter:site"
        content="@<%= site.data.owner.twitter %>">
<% } %>

I have an example using Jekyll, if you need example about twitter card.


7: Preview: Social Media

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

Telegram on Smartphone

Hexo Meta: Opengraph Telegram Smartphone

Slack on Smartphone

Hexo 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.


What is Next ?

Consider continue reading [ Netlify - Hosting ]. There are, some interesting topic about, how to make your blog site live.

Thank you for reading.