Our Website and some other websites we've built in JAMstack has been with the stack of: GitLab, Netlify, Gatsby and Netlify CMS. This allow us to build a website with React Components with content sourced in markdown files, managed by Netlify CMS and autodeployed using the Netlify service.
Our JAMstack
JAMstack stands for JavaScript, APIs, and Markup. The term was coined by Mathias Biilmann to describe a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
Netlify CMS
Netlify CMS is an open source content management system for your Git workflow that enables you to provide editors with a friendly UI and intuitive workflows. You can use it with any static site generator to create faster, more flexible web projects. Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git.
You may read more in the official documentation.
Gatsby
Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps. Forget complicated deploys with databases and servers and their expensive, time-consuming setup costs, maintenance, and scaling fears. Gatsby.js builds your site as “static” files which can be deployed easily on dozens of services.
You may read more in the official documentation.
Configuring Gatsby with Netlify CMS
Add the dependencies
First we need install some dependencies to make them work together
npm install --save netlify-cms-app gatsby-plugin-netlify-cms netlify-cms-app
Configuration for Netlify CMS
We need to create a file that tells to Netlify CMS which is our structure of data and where is stored, along with some other configuration. The file must be at static/admin/config.yml
├── static
│ ├── admin
│ │ ├── config.yml
backend:
name: gitlab
repo: alfchee/alfchee-site
branch: master
media_folder: "static/assets"
public_folder: assets
slug:
encoding: "ascii"
clean_accents: true
sanitize_replacement: "_"
collections:
- name: 'pages'
label: 'Pages'
folder: 'content/pages'
create: true
editor:
preview: false
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "string" }
- {
label: "Post Image",
name: "featuredImage",
widget: "image",
allow_multiple: false,
}
- { label: "Template Key", name: "templateKey", widget: "string" }
- { label: "Body", name: "body", widget: "markdown" }
With the previous we are configuring our backend to be GitLab and setting the repository name and the branch where Netlify CMS will be pushing changes.
Next we use media_folder
and public_folder
to tell where our assets will be stored when we upload files with the CMS, this come handy because we may specify the same directory used by Gatsby.
Then the Slug section allow us to create rules about how to create the slugs for the pages.
Lastly, in the section of collections
we may create as many collections we whish, and define the structure of data for them, in this way the CMS will show many fields as we specify. In this example we are creating a collection of pages, where they'll be stored as markdown files in the directory content/pages
, creating one file per page created. Then forl all the pages, the list of fields is declared. You may read more about the definition of collections and the available widgets to set the fields.
Accessing the content from Gatsby
Now that you have set your data and schema, you need to fetch it so can be rendereable, for so we need to make some configurations in Gatsby.
Dependencies
Make sure you have the next dependencies installed, they are Gatsby Plugins
npm install --save gatsby-source-filesystem gatsby-transformer-remark gatsby-remark-images gatsby-remark-responsive-iframe gatsby-remark-prismjs gatsby-remark-copy-linked-files gatsby-remark-smartypants gatsby-transformer-sharp gatsby-plugin-sharp prismjs gatsby-image
Configuring Gatsby
The Configuration of Gatsby is in the file gatsby-config.js
, and in that file we'll configure the source of data and also the plugins we've installed.
module.exports = {
/* Your site config here */
plugins: [
/* Pages */
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/content/pages`,
name: `pages`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 590,
},
},
{
resolve: `gatsby-remark-responsive-iframe`,
options: {
wrapperStyle: `margin-bottom: 1.0725rem`,
},
},
`gatsby-remark-prismjs`,
`gatsby-remark-copy-linked-files`,
`gatsby-remark-smartypants`,
],
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-netlify-cms`,
options: {
enableIdentityWidget: false,
},
},
"gatsby-plugin-netlify",
]
}
Each time you add a new directory with content that you need to be inspected by Gatsby then you'll need to add a block for gatsby-source-filesystem
and setting the path. But the markdown files are just text files, in order they can be processed correctly you need the plugin gatsby-transformer-remark
, that accepts some plugins to format the content, for so all the we've added in our configuration.
Then you can see that both plugins of Netlify has been added. The option enableIdentityWidget
is required to set to false
because we are using GitLab.
Static Pages in Gatsby
As you know, if you add files to src/pages
directory, Gatsby will try to render them as pages and create routes. For so src/pages/index.jsx
will be treated as the home page, and in the same way you can continue adding more static pages with static routes.
From a static page if you want to get information of your markdown files, you'll use GraphQL to query the data you need. For so you'll be using some objects provided by Gatsby. Example:
import { graphql } from "gatsby"
import React from "react"
class MyComponent extends React.Component {
render() {
// obtaining from props the data queried with GraphQL
const page = this.props.data.markdownRemark
}
}
export const query = graphql`
query MyPage{
markdownRemark({ WRITE YOUR QUERY }) {
id
html
frontmatter {
ANY OTHER FIELDS DEFINED
}
}
}
`
export default IndexPage
Dynamic Pages with Gatsby
Using the previous method we need to create new JS or JSX file for each page, and make the the same things once and another time. To avoid that we are going to dynamically create the pages, and to be able to do that we'll be using:
- Layout, defining one or more layouts that will define shared behavior for all the site
- Templates, defining shared behaviors or elements to be displayed in dependence of the page
- Edit
gatsby-node.js
, in this file we can use some hooks that allow us to create the pages dynamically
Install dependencies
npm install esm
Content for gatsby-node.js
If this file doesn't exist, create it or edit the content
require = require("esm")(module)
module.exports = require("./gatsby-node.esm.js")
Now we need to create the file gatsby-node.esm.js
and add the content
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
const buildPages = (edges, createPage) => {
const pages = edges.filter((edge) => {
if ("templateKey" in edge.node.frontmatter) {
return edge
}
})
// create pages with the filtered edges
pages.map((page) => {
console.log(`Creating page ${JSON.stringify(page.node.frontmatter)}`)
createPage({
path: page.node.fields.slug,
// getting the component to render the page using the templateKey property
component: path.resolve(
`src/templates/${String(page.node.frontmatter.templateKey)}.js`
),
context: {
id: page.node.id,
slug: page.node.fields.slug,
},
})
})
}
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
// graphql query for pages only
const result = await graphql(
`
{
allMarkdownRemark(
limit: 1000
filter: { fileAbsolutePath: { regex: "/(pages|blog)/i" } }
) {
edges {
node {
id
fields {
slug
}
frontmatter {
title
templateKey
}
}
}
}
}
`
)
// if errors then throw
if (result.errors) {
throw result.errors
}
console.log("Creating pages ---->>>>")
// Create pages
buildPages(result.data.allMarkdownRemark.edges, createPage)
}
The exposed function createPages
will create dynaic pages according our needs. Using GraphQL we are querying files that has the word page
or blog
in the path (not file name), and we are interested in the values id
, slug
, title
, and templateKey
.
We defined the logic in the function buildPages
to be able to handle more easy, we may create a function buildBlog
to create only blog post pages with a different route. The interesting here is that createPage
function needs the slug, which is the route for the URL and the component used to render this page, and is that the reason of the templateKey
field, that we are using it to determine the file to use as template. So for a default
templateKey
, the file to use as template is src/templates/default.js
.
The context is also important, so we make sure to pass the id
and slug
as context, because we can use them to query the content of the page we want to render.
Creating Templates
We may create many templates as we need to display or hide components or data in the dynamic pages, and we are going to define a default template. Create the file src/templates/default.js
with the content
import React from "react"
import { graphql } from "gatsby"
import { Container } from "react-bootstrap"
import Layout from "../components/Layout"
import NavBar from "../components/NavBar"
import Footer from "../components/Footer"
import styles from "./default.module.css"
class PageDefaultTemplate extends React.Component {
render() {
const page = this.props.data.markdownRemark
const meta = {
pageTitle: page.frontmatter.title,
}
// TODO: Get the navbar items
return (
<Layout siteMeta={meta}>
{/* Page content */}
<section className={styles.sectionTerms}>
{/* Navbar */}
<NavBar />
{/* end of Navbar */}
<Container className={styles.contentData}>
<header>
<h1 className={styles.title}>{page.frontmatter.title}</h1>
</header>
<article
className={styles.dataInfo}
dangerouslySetInnerHTML={{ __html: page.html }}
/>
</Container>
{/* Footer */}
<Footer />
</section>
{/* end of Page content */}
</Layout>
)
}
}
export default PageDefaultTemplate
export const pageQuery = graphql`
query PageBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
id
html
frontmatter {
description
featuredImage
title
templateKey
}
}
}
`
As seeing, the slug
passed as context is used here to make a query in the markdownRemark
collection to get the content of the page in which we are navigating, and then we can use it as we whish.
Layout component
Is good to have a layout component that handles SEO among other stuffs, but remember to have this kind of content in your layout
const Layout = ({ children, siteMeta }) => {
const { pageTitle } = siteMeta
return (
<>
<Seo pageTitle={pageTitle} />
{/* main container for all the pages content */}
<div className="main-container">{children}</div>
</>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
siteMeta: PropTypes.object,
}
Top comments (1)
Thank you very much for writing this.
I haven't implemented my version of this, but it looks to me this is the article I'm looking for.