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.
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.
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.
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?
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:
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 ;)
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
html
for each nodeactions.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.
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!