ssg  
Where to Discuss?

Local Group

Preface

Goal: Refactoring special page, Index, Archive, Category, and Tag.

Hexo has this four special page sections: Index, Archive, Category, Tag. The last three has very parts in common. So we can redefine as:

  • 1: Blog Index

  • 2: Section List

Source Code

You can download the source code of this article here.

Extract and run on CLI:

$ npm install

1: Blog Index

Special Layout: Index

Remember our last blog index. Let me refresh our setup.

Configuration

We need to setup permalink and stuff. Add these lines below our configuration.

# Home page setting
index_generator:
  path: '/blog'
  order_by: -date

Layout: EJS Index: Redesigned

We can rewrite blog template. This time using my own custom class meta-item blog-item.

  • themes/tutor-04/layout/index.ejs
<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h1 class="title is-4"><%= config.author %></h1>
    <h2 class="subtitle is-4"><%= config.subtitle %></h2>
  </section>

  <div class="post-list">
  <% page.posts.each(function(post){ %>
    <section class="section" id="archive">
    <div class="meta-item blog-item">
      <a href="<%- url_for(post.path) %>"><%= post.title %></a>
    </div>
    </section>
  <% }) %>
  </div>

</main>

Render: Browser

Open in your favorite browser.

Hexo: Blog Index Redesign

Layout: EJS Index: Refactored

Things get complex when we design the inner parts. This why refactoring takes place.

<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h1 class="title is-4"><%= config.author %></h1>
    <h2 class="subtitle is-4"><%= config.subtitle %></h2>
  </section>

  <div class="post-list">
  <% page.posts.each(function(post){ %>
    <section class="section" id="archive">
      <%- partial('summary/blog-item', {post: post}) %>
    </section>
  <% }) %>
  </div>

</main>

Layout: EJS Blog Item

For each item, processed in its own template.

    <div class="meta-item blog-item">

      <strong><a class="meta_link" 
        href="<%- url_for(post.path) %>"><%= post.title %>
      </strong></a>

      <div class="meta">
        <div class="meta_time is-pulled-left">
          <i class="fa fa-calendar"></i>
          <time datetime="<%= date(post.date, "YYYY-MM-DD[T]HH:mm:ss.SSS") %>">
          <%= date(post.date, "MMM DD, YYYY") %></time>
        </div>      
        <div class="is-pulled-right" id="meta_tags">
          <% post.tags.forEach(function(tag){ %>
            <a href="<%- url_for(tag.path) %>">
              <span class="tag is-dark">
                <span class="fa fa-tag"></span>
                &nbsp;<%= tag.name %></span></a>
          <% }) %>
        </div>
      </div> 

      <div class="is-clearfix"></div>
      
      <div>
        <% if(post.excerpt) { %>
        <p><%- post.excerpt %></p>
        <% } %>
      </div>

      <div class="read-more is-light">
        <a href="<%- url_for(post.path) %>" 
           class="button is-dark is-small">Read More&nbsp;</a>
      </div>

    </div>

Basically, just moving the complex part somewhere else.

Render: Browser

Open in your favorite browser.

Hexo: Blog Index Refactored


2: Simple Section

Special Layout: Archive, Category, Tag

What can be applied to one layout, can be applied to another. We are going to start with archive layout.

Layout: EJS Archive

Consider slight modification of our previous layout.

<% 
  var section_title = __('archive');

  if (is_month()){
    section_title += ': ' + page.year + '/' + page.month;
  } else if (is_year()){
    section_title += ': ' + page.year;
  }
%>
<%-
  partial('summary/post-list-simple', {
    section_title: section_title
  })
%>

.

Layout: EJS Post List: Simple

<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h4 class="title is-4"><%= section_title %></h4>
  </section>

  <section class="section" id="archive">
  <% page.posts.each(function(post){ %>
    <div class="archive-item">
      <div class="is-pulled-left"><a href="<%- url_for(post.path) %>">
        <%= post.title %>
      </a></div>
      <div class="is-pulled-right has-text-right"><time>
          <%= date(post.date, "DD MMM") %>&nbsp;
          <span class="fa fa-calendar"></span>
      </time></div>
      <div class="is-clearfix"></div>
    </div>
  <% }) %>
  </section>
</main>

Render: Browser

Open in your favorite browser. You should see, the modified version of archive page.

Hexo Bulma: Archive Page: Simple

The Loop

How Does It Works?

The simplified range loop, is as below:

  <% page.posts.each(function(post){ %>
    ...
    <%= post.title %>
    ...
  <% }) %>

3: Grouping Query: By Year

How about grouping the list above by year? Thi would be complicated in other templating engine. Luckily we are using embedded javascript.

Data Structure

Suppose we have an array of posts:

.
├── [post 01]
│   ├── title
│   ├── name
│   ├── ...
│   └── date
└── [post 01]
    ├── title
    ├── name
    ├── ...
    └── date

First we have to insert year as field in every item. So we have something like this:

.
├── [post 01]
│   ├── title
│   ├── name
│   ├── ...
│   ├── date
│   └── year
└── [post 01]
    ├── title
    ├── name
    ├── ...
    ├── date
    └── year

We can achieve this by using mapping in javascript.

  // grouping by date

  const posts = page.posts.map(post => ({ 
    ...post, year: date(post.date, "YYYY")
  }));

As we already have year field, we can group this data structure by year.

  const postsByYear = groupBy(posts, 'year');

While the groupBy function, is provided in mozilla site:

Javascript: EJS Post List: By Year

The complete javascript source is as below:

  // Data Model

  // https://developer.mozilla.org/.../Array/reduce
  
  function groupBy(objectArray, property) {
    return objectArray.reduce(function (acc, obj) {
      var key = obj[property];
      if (!acc[key]) {
        acc[key] = [];
      }
      acc[key].push(obj);
      return acc;
    }, {});
  }

  // grouping by date

  const posts = page.posts.map(post => ({ 
    ...post, year: date(post.date, "YYYY")
  }));

  const postsByYear = groupBy(posts, 'year');

Layout: EJS Post List: By Year

Pour down the javascript above, between the tag below <% … _%>:

<%
  ...
_%>
<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h4 class="title is-4"><%= section_title %></h4>
  </section>

  <% 
    const postsSorted = Object.keys(postsByYear).sort(date, -1);
    postsSorted.forEach(function (year){ 
  %>
  <section class="section box">
    <div class ="anchor-target archive-year" 
         id="<%= year %>"><%= year %></div>

    <div class="archive-list archive-p3">
      <% postsByYear[year].forEach(function(post){ %>
      <div class="archive-item">
        <div class="is-pulled-left"><a href="<%- url_for(post.path) %>">
          <%= post.title %>
        </a></div>
        <div class="is-pulled-right has-text-right"><time>
            <%= date(post.date, "DD MMM") %>&nbsp;
            <span class="fas fa-calendar"></span>
        </time></div>
        <div class="is-clearfix"></div>
      </div>
      <% }) %>
    </div>
  </section>
  <% }) %>
</main>

Render: Browser

Open in your favorite browser. You should see, the archive page grouped by year.

Hexo Bulma: Archive Page: By Year

The Loop

How Does It Works?

The simplified range loop, is as below:

  <% 
    const postsSorted = Object.keys(postsByYear).sort(date, -1);
    postsSorted.forEach(function (year){ 
  %>
    ...
    <%= year %>
    ...
    <% postsByYear[year].forEach(function(post){ %>
      ...
      <%= post.title %>
      ...
    <% }) %>
    ...
  <% }) %>

While postsByYear is:

  const postsByYear = groupBy(posts, 'year');

I hope that, this explanation is clear.

Update

For unknown reason. Hexo break break my logic. This .sort(date, -1) is no longer working.

The solution is to use .sort(date, -1).reverse().


4: Grouping Query: By Month

How about grouping the list above by month? How complex this would be?

Data Structure

Again, suppose we have an array of posts:

.
├── [post 01]
│   ├── title
│   ├── name
│   ├── ...
│   └── date
└── [post 01]
    ├── title
    ├── name
    ├── ...
    └── date

First we have to insert both month and year as field in every item. Wait… why both year and month? Because we have two loops. The outer loop would process by year, and the inner one wouyld process by month.

We have should have something like this:

.
├── [post 01]
│   ├── title
│   ├── name
│   ├── ...
│   ├── date
│   ├── year
│   └── month
└── [post 01]
    ├── title
    ├── name
    ├── ...
    ├── date
    ├── year
    └── month

We can achieve this by using mapping in javascript. Actually this is almost exactly the same as our previous example. I guess, this is not so complicated, as I thought it was.

  const posts = page.posts.map(post => ({ 
    ...post,
    year: date(post.date, "YYYY"),
    month: date(post.date, "MMMM")
  }));

As we already have year field, we can group this data structure by year.

  const postsByYear = groupBy(posts, 'year');

Wait..? Where is the group by month? Well…. this will be processed in inner loop later.

Javascript: EJS Post List: By Year

The complete javascript soource is as below:

  // Data Model

  // https://developer.mozilla.org/.../Array/reduce
  
  function groupBy(objectArray, property) {
    return objectArray.reduce(function (acc, obj) {
      var key = obj[property];
      if (!acc[key]) {
        acc[key] = [];
      }
      acc[key].push(obj);
      return acc;
    }, {});
  }

  // grouping by date

  const posts = page.posts.map(post => ({ 
    ...post,
    year: date(post.date, "YYYY"),
    month: date(post.date, "MMMM")
  }));

  const postsByYear = groupBy(posts, 'year');
  
  // additional
  
  const yearToday = date(new Date(), 'YYYY');

Layout: EJS Post List: By Month

Pour down the javascript above, between the tag below <% … _%>:

<%
  ...
_%>

<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h4 class="title is-4"><%= section_title %></h4>
  </section>

  <% 
    const postsSorted = Object.keys(postsByYear).sort(date, -1);
    postsSorted.forEach(function (year){ 
  %>
  <section class="section box">
    <div class ="anchor-target archive-year" id="<%= year %>">
       <% if (year == yearToday) {%>
         This year's posts (<%= year %>)
       <% } else { %>
         <%= year %>
       <% } %>
    </div>
    <%
      const postsByMonth = groupBy(postsByYear[year], 'month');
      Object.keys(postsByMonth).forEach(function (month){
    %>
    <div class="archive-p4">
      <div class ="archive-month" 
           id="<%= year %>-<%= month %>">
           <%= month %> <%= year %></div>

      <div class="archive-list archive-p3">
        <% postsByMonth[month].forEach(function(post){ %>
        <div class="archive-item">
          <div class="is-pulled-left"><a href="<%- url_for(post.path) %>">
            <%= post.title %>
          </a></div>
          <div class="is-pulled-right has-text-right"><time>
              <%= date(post.date, "DD MMM") %>&nbsp;
              <span class="fas fa-calendar"></span>
          </time></div>
          <div class="is-clearfix"></div>
        </div>
        <% }) %>
      </div>

    </div>
    <% }) %>
  </section>
  <% }) %>
</main>

Render: Browser

Open in your favorite browser. You should see, the archive page grouped by month.

Hexo Bulma: Archive Page: By Month

The Loop

How Does It Works?

The simplified range loop, is as below:

  <% 
    const postsSorted = Object.keys(postsByYear).sort(date, -1);
    postsSorted.forEach(function (year){
  %>
    ...
    <%= year %>
    <%
      const postsByMonth = groupBy(postsByYear[year], 'month');
      Object.keys(postsByMonth).forEach(function (month){
    %>
      ...
      <%= month %>
      ...
      <% postsByMonth[month].forEach(function(post){ %>
        ...
        <%= post.title %>
        ...
      <% }) %>
      ...
    <% }) %>
    ...
  <% }) %>

Update

For unknown reason. Hexo break break my logic. This .sort(date, -1) is no longer working.

The solution is to use .sort(date, -1).reverse().


5: Responsive

Doing responsive is just a matter of changine view, without altering the javascript data model.

Layout: EJS Post List: Responsive

Pour down the javascript above, between the tag below <% … _%>:

<%
  ...
_%>
<main role="main" 
      class="column is-full box-deco has-background-white">
  <section class="section">
    <h4 class="title is-4"><%= section_title %></h4>
  </section>

  <% 
    const postsSorted = Object.keys(postsByYear).sort(date, -1);
    postsSorted.forEach(function (year){
  %>
  <section class="section">

  <div class ="anchor-target archive-year" id="<%= year %>">
     <% if (year == yearToday) {%>
       This year's posts (<%= year %>)
     <% } else { %>
       <%= year %>
     <% } %>
  </div>

  <div class="columns is-multiline ">
    <%
      const postsByMonth = groupBy(postsByYear[year], 'month');
      Object.keys(postsByMonth).forEach(function (month){
    %>

    <div class="column is-full-mobile 
                is-half-tablet is-one-third-widescreen">

      <section class="panel is-light">
        <div class="panel-header" id="<%= year %>-<%= month %>">
          <p><%= month %> <%= year %></p>
          <span class="fa fa-archive"></span>
        </div>
        <div class="panel-body has-background-white">

          <div class="archive-list archive-p3">
            <% postsByMonth[month].forEach(function(post){ %>
            <div class="archive-item">
              <div class="is-pulled-left"><a href="<%- url_for(post.path) %>">
                <%= post.title %>
              </a></div>
              <div class="is-pulled-right has-text-right"><time>
                  <%= date(post.date, "DD MMM") %>&nbsp;
                  <span class="fas fa-calendar"></span>
              </time></div>
              <div class="is-clearfix"></div>
            </div>
            <% }) %>
          </div>

        </div>
      </section>

    </div>

    <% }) %>
  </div>
  </section>
  <% }) %>
</main>

How Does It Works?

These bulma classes below do all the hardworks:

  • is-full-mobile

  • is-half-tablet

  • is-one-third-widescreen

  <section class="columns is-multiline" id="archive">
  <% terms.each(function(item){ %>
    <div class="column
                is-full-mobile
                is-half-tablet
                is-one-third-widescreen">
       ...
    </div>
  <% }) %>
  </section>

Now we can use them for

  • Archive section page,

  • Category section page, and

  • Tag section page.

Consider have a look at difference for each screen size.

Layout: EJS Archive

<% 
  var section_title = __('archive');

  if (is_month()){
    section_title += ': ' + page.year + '/' + page.month;
  } else if (is_year()){
    section_title += ': ' + page.year;
  }
%>
<%-
  partial('summary/post-list-responsive', {
    section_title: section_title
  })
%>

Open in your favorite browser.

Hexo: Browser Section Archive

.

Layout: EJS Category

<%- 
  partial('summary/post-list-responsive', {
    section_title: __('category') + ': ' + page.category
  })
%>

Open in your favorite browser.

Hexo: Browser Section Category

.

Layout: EJS Tag

<%- 
  partial('summary/post-list-responsive', {
    section_title: __('tag') + ': ' + page.tag
  })
%>

Open in your favorite browser.

Hexo: Browser Section Tag

.

I think that’s all about this article.


What is Next ?

There are, some interesting topic about Custom Output in Hexo. Consider continue reading [ Hexo - Custom Output ].

Thank you for reading.