December 04, 2023

Let's make a simple blog

Separating content from code is always a good idea. Therefore, let's explore how to do that in a GatsbyJS website. Without any external data sources.

Different content same layout

Most websites will start as a collection of one-of pages. Each page will be carefully design separately. Then there comes to time where we need to add a bunch of content elements that are of the same type. Like a list of blog posts, technical articles, Q&A pages, news or past or upcoming events. Creating a design for each blog or event info would be a massive overkill.

Of course, Gatsby already provides full support for scenario, giving you many many options.

In this post we will look at a very simple scenario, which (I hope) you can easily extend depending on your needs.

Blogs and Blog

The title of the post already gave away what we are up to, so we'll just dive right in.

First of all, we don't want to use any particular data source for our blog post. Instead we will just create plain files in our website folder.

Then we want to have two pages. One to show a list of pages and a second that will be a template for all blog posts.

Files as Content

Markdown files are plain text files, with very simple formatting instructions.

# this is H1 heading
## this is H2 heading
*bold text*

GatsbyJS comes with support for markdown files out of the box. If it sees an .md file it will create a node automatically (and will even process the instructions!). They are ideal for our simple example.

Creating a blog post is just creating a file. We will put them into the src/content/ directory.

/src/content/cms_blog.md

---
slug: "cms-blog"
date: "2023-12-04"
title: "Let's make a trivial blog"
---
# Let's make a simple blog
...

That was easy right?

List of Blog posts

GatsbyJS organizes data into a node graph. Each of our blog-post/md-file becomes a node. The only thing we need to do, is to create a Blog page and render the nodes.

For the loading of the nodes we will export a query from our Blogs.tsx page.

export const pageQuery = graphql`
query {
	allMarkdownRemark(sort: {frontmatter: {date: DESC}}) {
	  edges {
		node {
		  id
		  excerpt(format: HTML, pruneLength: 280)
		  frontmatter {
			date(formatString: "MMMM DD, YYYY")
			title
			slug
		  }
		}
	  }
	}
  }
`

It is a bit convoluted so we'll dissect the query. allMarkdownRemark is the name of node-type under which Gatsby will collect all our md files. frontmatter is a special subnode that represents the md file header. This node contains the attributes you put between the --- separators.

In plain english, we tell Gatsby to give us:

  1. all md files
  2. sorted by the header date attribute, newest first
  3. from the files get the internal node id, the excerpt (first 280 characters of our blog post formatted as html).
  4. and get the header attributes date, title and slug.

Gatsby will execute the query and pass the results to our page component.

And here is the rendering part.

import * as React from "react"
import { graphql } from "gatsby"

const AllBlogsPage = (props: {
	data: any, // this prop will be injected by the GraphQL query below.
}) => {
	const edges = props.data.allMarkdownRemark.edges as any[];
	return (<>
		<div className="hero">
			Blogs
		</div>

		<div className="section blogSection">
			{edges.map(x => {
				const n = x.node;
				const url = "/blog/" + n.frontmatter.slug;
				const html = n.excerpt;
				return <div>
					<h2><a href={url}>{n.frontmatter.title}</a></h2>
					<div>
						<div className="blogDate">{n.frontmatter.date}</div>
						<div className="blogExcerpt" dangerouslySetInnerHTML={{ __html: html }}></div>
					</div>
				</div>
			})}
		</div>
	</>
	)
}

The AllBlogsPage component simply iterates over all blogs and creates a small div for each one of them. We are using the built-in support for excerpt to only show the first couple of words from the post.

We also render a link, so the user can read the rest of the post. Observe that we are using the slug md file header attribute to create the link. Slug is the web-slang word for the human & machine readable url identified. We could just as easily used the internal id, or even the file name. But google is particular about human readable urls and we all wish to make google happy ;)

The blog post Template

We now want to create a page that will be used for all blog post urls. And it shall render the correct blog post of course.

The easy was is to use collection routes. However in our example we'll do it the slightly more complicated way. The reason is, that if you are using the gatsby-plugin-i18n plugin, the collection routes don't work:(

To render a page for each blog we'll use the createPages callback in gatsby-node.ts file.

import { GatsbyNode } from "gatsby"
import path from "path";

export const createPages: GatsbyNode["createPages"] = async ({ actions, graphql }) => {
	
	const { data } = await graphql(`
		  query {
			allMarkdownRemark {
			  edges {
				node {
					frontmatter {
						slug
						date(formatString: "MMMM DD, YYYY")
				  }
				  html
				}
			  }
			}
		  }
		`as any) as any;
	data.allMarkdownRemark.edges.forEach((edge:any) => {
		const slug = edge.node.frontmatter.slug
		const url = "/blog/" + slug;
		actions.createPage({
			path: url,
			component: path.resolve(`./src/templates/blog.tsx`),
			context: { node: edge.node },
		})
	})
}

This is doing three things

  1. Loads all md file nodes (just like our AllBlogsPage)
  2. Asks for the html for each node
  3. Call actions.createPage and thus creating a page for each node.

As you can see we are delegating the actual rendering to a component ```blog.tsx``

component: path.resolve(`./src/templates/blog.tsx`),

The component itself is fairly trivial as you can see

import * as React from "react"
import { graphql } from "gatsby"
import { Header } from "../components/Header";
import { Footer } from "../components/Footer";
import "../components/styles.css";

const BlogPostTemplate = (props: any) => {
  const { frontmatter, html } = props.pageContext.node;
  return (
      <div className="blogPost>
        <div>
          <h1>{frontmatter.title}</h1>
          <div className="blogBodyDate">{frontmatter.date}</div>
          <div
            dangerouslySetInnerHTML={{ __html: html }}
          />
        </div>
      </div>
  )
}

export default BlogPostTemplate;

The dangerouslySetInnerHTML attribute is a React specific way to set the inner content of the div to the html we provide. It sounds scary, because should only ever do this with content you fully control. Since we know the html comes from our transformed md file, we know it won't contain any scripts or any other nasty surprises.

Finally

We have created a simple blog inside our Gatsby website. A reader can see all our blogs at a glance and can drill-down to a particularly interesting one. All pages are pre-rendered and thus fully searchable by google.

I also wanted to point out that markdown is a good compromise between html and plain text. And also fully supported by GatsbyJS, so it just works.

We also used the extremely powerful createPage API. It allow us to automate the technical details of combining multiple content pieces with a single template. And much, much more.

Happy hacking!