After the recent migration from Medium to Hugo I continued digging into the JAM stack and GatsbyJS, as having a statically generated site for my blog is working well so far.
I could have taken a ready template to mimic design and keep content and make another quick release. Though, for this migration, I decided to spent time taking concrete steps I consider reusable:
This article will highlight lessons learned about these steps with the aim to provide high-level guidelines about patterns which can be re-used in migrations with other generators to GatsbyJS.
Hugo is a really super fast, convenient and well-supported tool for working with static sites. I think today it’s still more mature and closer to classical CMSs than GatsbyJS workflow-wise. This means that at this time, when you need a ready plugin or a theme for quick gratification, it’s more likely to find something ready online.
Whereas, GatsbyJS is based on React, GraphQL, Webpack and the way of thinking is closer to how a developer from MEAN/SPA stack would approach problems. It’s also a bit more “raw” and there are starters and typography.js, but not so many ready solutions in the conventional sense.
If you have landed at this article researching which tool is better for your job, take a look at comparisons and keep in mind that selecting a stack boils down to being effective with it.
For me, GatsbyJS is valuable learning experience and it has also been so easy to work with, it feels “unfair”. For example, the plugin system of GatsbyJS keeps me sane and productive, even at cases I know only the brief overview concepts of Webpack, whereas others have spent hours and days configuring what I get out of the box to be productive.
This task was easier than expected. The file structure is preserved between my previous blog and the current version. Both Hugo and GatsbyJS work well when markdown files are stored in flat at content/post
folder in my repository.
The only work I had to do on the content “migration” was to reformat the frontmatter. In Hugo, I used TOML, whereas gatsby-transformer-remark
works only with YAML for the moment. Luckily, I still had the cli of Hugo on my system and could make use of the build-in conversion tool. I guess this would be a bit less straight-forward in more sophisticated and nested hierarchy structures. In my case, the only issue I had was that sometimes titles were longer than 1 line and were not parse-able by the GatsbyJS build, so I just had to cut some words out where problematic.
Last word about this step is that my previous frontmatter already contained title
, date
, tags
, and most importantly - the slug
fields. These were enough for my later work on the programatic creation of content explained in the next section.
This is the official documentation, plus there is a tutorial which gives examples. Basically, I had to create a gatsby-node.js
file which exports createPages
method using the createPage
action from boundActionCreators
.
This might sound way more complicated than what it is:
exports.createPages = ({ graphql, boundActionCreators }) => {
const { createPage } = boundActionCreators;
graphql(`
{
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
edges {
node {
frontmatter {
title
slug
tags
}
}
}
}
}
`).then((result) => {
const posts = result.data.allMarkdownRemark.edges;
// Create content programatically here
});
};
As you see, getting the list of posts can be done in a single query.
The result of this query can later be handled by a “creator” function, which I prefer to keep in a separate module. For example, creating posts works like following:
const path = require(`path`);
module.exports = (createPage, nodes) => {
const template = path.resolve(`src/templates/post.js`);
nodes.map(({ node }) => {
if (node.frontmatter.slug) {
createPage({
path: node.frontmatter.slug,
component: template,
context: {
slug: node.frontmatter.slug,
},
});
}
});
};
I re-use the slug
field of the frontmatter of my existing structure. I don’t have to generate or calculate slugs based on information of other fields, i.e. my scenario is easier than the tutorial on the official docs.
This is an example of “unfair” easy - I don’t have to do literally anything to keep my previous URLs of existing content the same in the new system.
The display of the data is handled by a React component acting as a template. My case is nothing different than the official documentation, nothing exotic.
Now that I got a decent grasp of how to create content in my new site, I proceeded with creating pagition. I have about 30 blog posts, so I went for a split by 10 to give an impression I have a lot of content :)
As usual, a good starting point was searching for example implementations available in examples
and the issue queue. There, in the issue queue, is a gem epic about plugins wishlist where I found the discussion leading to gatsby-paginate.
As I wanted to have different contexts than the plugin, so I took inspiration for both tags and pagination scenarios. I kept them as separate action creators and I just called them in the main creator function like this:
const createPostPages = require(`./gatsby-actions/createPostPages`);
const createPaginatedPostsPages = require(
`./gatsby-actions/createPaginatedPostsPages`,
);
const createTagPages = require(`./gatsby-actions/createTagPages`);
exports.createPages = ({ graphql, boundActionCreators }) => {
const { createPage } = boundActionCreators;
graphql(`
{
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
edges {
node {
frontmatter {
title
slug
tags
}
}
}
}
}
`).then((result) => {
const posts = result.data.allMarkdownRemark.edges;
createPostPages(createPage, posts);
createPaginatedPostsPages(createPage, posts);
createTagPages(createPage, posts);
});
};
Easy to read, understand and mantain. The pagination module is a bit longer than the one of the posts:
const path = require(`path`);
module.exports = (createPage, nodes) => {
const template = path.resolve(`src/templates/postList.js`);
const paginateSize = 10;
// Split posts into arrays of length equal to number posts on each page/paginateSize
const groupedPages = nodes
.map((node, index) => {
return index % paginateSize === 0
? nodes.slice(index, index + paginateSize)
: null;
})
.filter((item) => item);
// Create new indexed route for each array
groupedPages.forEach((group, index, groups) => {
const pageIndex = index === 0 ? `` : index + 1;
const paginationRoute = `/blog/${pageIndex}`;
// Avoid showing `Previous` link on first page - passed to context
const first = index === 0 ? true : false;
// Avoid showing `Next` link if this is the last page - passed to context
const last = index === groups.length - 1 ? true : false;
return createPage({
path: paginationRoute,
component: template,
context: {
group,
first,
last,
index: index + 1,
},
});
});
};
Then, pull context information in the React component:
const BlogPagedIndex = ({ pathContext }) => {
const { group, index, first, last } = pathContext;
return (
<div>
// Some elements
...
// The posts
<ul>
{group.map((node, key) => <Post key={key} node={node} />)}
</ul>
// The pager
<div>
{!first && (
<Link to={`/blog/${index > 2 ? index - 1 : ''}`}>Newer posts<Link>
)}
{!last && (
<Link to={`/blog/${index + 1}`}>Older posts</Link>
)}
</div>
</div>
);
};
export default BlogPagedIndex;
This is a cut-down version of the component only for the blog post, do not copy with too much trust …
I have to be honest that I haven’t made pagination in React/Redux, but I feel this pagination approach would be comparatively easier. Also, I want the pagination pages to be accessible at all times, not only on state change, so the content creation approach of building the list works well for me.
I will say again that I see this is “unfair” easy. It’s probably the quickest implementation of pagination I’ve made in my life. I believe GraphQL pagination with caching and Redux, etc. would be a more sophisticated way to solve the problem ;)
For the list of tags and inner tags pages, the approach was similar but passing different context to the template component:
For the overview page of tags:
createPage({
path: `/tags`,
component: template,
context: {
posts,
},
});
For the inner tag page:
createPage({
path: `/tags/` + slugify(tagName),
component: template,
context: {
posts,
post,
tag: tagName,
},
});
I have another blog post on the topic so I won’t go into too many details.
I tried to use the git-gateway
identity management approach in Netlify, but it didn’t work for me. I could not reach the point to validate or reset the password for my user 1, so I kept the “old-school” way of github integration which works just well for me at the moment, having the fact I will be 1 user to work on the site.
Not to mention also that I add this admin panel mostly for demoing the concept of JAM stack with admin panel to colleagues and friends, not to actually write there so much as I’m using Atom.
Long story short, this is the config.yml
configuration file:
backend:
name: github
repo: kalinchernev/kalinchernev.github.io # Path to your Github repository
branch: gatsby # Branch to update (master by default)
publish_mode: editorial_workflow
media_folder: "static/images" # Folder where user uploaded files should go
collections: # A list of collections the CMS should be able to edit
- name: "post" # Used in routes, ie.: /admin/collections/:slug/edit
label: "Post" # Used in the UI, ie.: "New Post"
folder: "content/post" # The path to the folder where the documents are stored
sort: "date:desc" # Default is title:asc
create: true # Allow users to create new documents in this collection
slug: "{{slug}}"
fields: # The fields each document in this collection have
- { label: Title, name: "title", widget: "string", tagname: "h1" }
- { label: "Date", name: "date", widget: "datetime" }
- { label: Slug, name: "slug", widget: "string" }
- {
label: Tags,
name: tags,
widget: list,
default: ["APIs", "JavaScript"],
}
- { label: "Body", name: "body", widget: "markdown" }
The only interesting part is the gatsby
branch which I used in parallel to the blog
branch. The gatsby
branch is my development/staging and blog
is my production.
This is my admin page React component which is placed in src/pages/admin
so that GatsbyJS delivers the HTML page at /admin
.
import React from "react";
import Helmet from "react-helmet";
const AdminPage = () => (
<div className="admin">
<Helmet>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
<link
rel="stylesheet"
href="https://unpkg.com/netlify-cms@^0.5.0/dist/cms.css"
/>
<script
type="text/javascript"
charSet="utf-8"
async
src="https://unpkg.com/netlify-cms@^0.5.0/dist/cms.js"
/>
</Helmet>
</div>
);
export default AdminPage;
In order for NetlifyCMS script to find the configuration file correctly, config.yml
should be placed in static/admin/config.yml
.
Any other location or file name will result in an error.
In this blog post I shared how a migration from one static generator as Hugo can work towards GatsbyJS. The reasons for doing a migration like this are a commbination of development-time benefits (easier development) and also a better production build of a static site which feels as smooth as a single page application.
We also went through the few technical details necessary to realize the migration, using GraphQL query, creators and templates. Lastly, we added an admin panel to brag to others about that content management part of our static site can be as easy as the development part of it ;)
Enjoy!