Gatsby Essentials
Gatsby Essentials Gatsby is a React framework for building fast, content-driven static sites — marketing sites, blogs, docs, e-commerce catalogs. Its defining f…
Gatsby Essentials
Gatsby is a React framework for building fast, content-driven static sites — marketing sites, blogs, docs, e-commerce catalogs. Its defining feature is the GraphQL data layer: instead of fetching data at request time, Gatsby pulls data from any number of sources (Markdown/MDX files, a CMS, REST/GraphQL APIs) into a single unified GraphQL schema at build time, then generates static HTML for every page from it. This trades runtime flexibility for build-time speed and reliability — the tradeoff that made it popular for JAMstack sites before Next.js's hybrid rendering ate into that niche.
The GraphQL Data Layer
Source plugins (`gatsby-source-filesystem`, `gatsby-source-contentful`, etc.) pull raw data into Gatsby's internal GraphQL store during the build. Transformer plugins (`gatsby-transformer-remark` for Markdown, `gatsby-plugin-image` for images) reshape that raw data into queryable nodes. You then query it with a `page query` (top-level, in a page component) or a `StaticQuery`/`useStaticQuery` (in any component, including non-page components like layouts).
// gatsby-config.js
module.exports = {
siteMetadata: {
title: 'My Gatsby Blog',
siteUrl: 'https://example.com',
},
plugins: [
'gatsby-plugin-image',
'gatsby-plugin-sharp',
{
resolve: 'gatsby-source-filesystem',
options: { name: 'posts', path: `${__dirname}/content/posts` },
},
'gatsby-transformer-remark', // Markdown -> MarkdownRemark nodes
{
resolve: 'gatsby-source-contentful',
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
},
},
],
}// src/templates/blog-post.jsx
import { graphql } from 'gatsby'
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
export default function BlogPost({ data }) {
const { markdownRemark } = data
const heroImage = getImage(markdownRemark.frontmatter.hero)
return (
<article>
<h1>{markdownRemark.frontmatter.title}</h1>
<GatsbyImage image={heroImage} alt={markdownRemark.frontmatter.title} />
<div dangerouslySetInnerHTML={{ __html: markdownRemark.html }} />
</article>
)
}
// Page query — runs at build time, result is injected as the `data` prop
export const query = graphql`
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
date(formatString: "MMMM D, YYYY")
hero {
childImageSharp {
gatsbyImageData(width: 1200, placeholder: BLURRED)
}
}
}
}
}
`Because the query runs at build time, there's no loading state to handle in the component — `data` is always populated before the page is rendered. This is the biggest mental shift coming from client-side data fetching: you describe what data a page needs declaratively, and Gatsby figures out how to get it before render, not after.
gatsby-node.js: Programmatic Page Creation
Pages under `src/pages/` are created automatically by filesystem routing, but for data-driven pages (one page per blog post, per product) you generate them programmatically in `gatsby-node.js` using the `createPages` API, querying your data layer and calling `createPage` for each result.
// gatsby-node.js
const path = require('path')
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const blogPostTemplate = path.resolve('./src/templates/blog-post.jsx')
const result = await graphql(`
query {
allMarkdownRemark {
nodes {
fields { slug }
}
}
}
`)
if (result.errors) {
reporter.panicOnBuild('Error loading markdown for page creation', result.errors)
return
}
result.data.allMarkdownRemark.nodes.forEach((node) => {
createPage({
path: node.fields.slug,
component: blogPostTemplate,
context: { slug: node.fields.slug }, // passed as $slug to the page query
})
})
}
// onCreateNode — runs per raw node, used to derive the slug field above
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === 'MarkdownRemark') {
const slug = `/blog/${path.basename(node.fileAbsolutePath, '.md')}/`
createNodeField({ node, name: 'slug', value: slug })
}
}
// createSchemaCustomization — lock down types so queries fail fast on typos
exports.createSchemaCustomization = ({ actions }) => {
actions.createTypes(`
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
type Frontmatter {
title: String!
date: Date @dateformat
hero: File @fileByRelativePath
}
`)
}SSG vs DSG vs SSR
Gatsby is best known for pure SSG (Static Site Generation — every page pre-rendered at build time), but per-page rendering options let you opt individual pages out of that when the dataset is too large to build in full or needs to be fresh on every request.
// src/templates/product.jsx
import { graphql } from 'gatsby'
export default function Product({ data }) { /* ... */ }
export const query = graphql`
query ($id: String!) {
product(id: { eq: $id }) { name price description }
}
`
// SSG (default) — pre-rendered at build time, fastest, needs a full rebuild to update
// DSG — Deferred Static Generation: page is generated on first request after
// deploy, then cached like a normal static page. Good for thousands of rarely
// visited pages (long-tail product pages) where building all of them up front
// would blow up build time.
export async function config() {
return { defer: true }
}
// SSR — Server-Side Rendering: renders on every request, for data that must be
// fresh (stock levels, personalized content). Use getServerData in the page module.
export async function getServerData({ params }) {
const res = await fetch(`https://api.example.com/products/${params.id}`)
return { props: await res.json() }
}SSG — build-time HTML for every page. Best performance, but a single content change triggers a full rebuild/redeploy.
DSG — generated on first request post-deploy, then served statically. Keeps huge sites' build times manageable.
SSR — rendered per request via `getServerData`. Use sparingly — it forfeits Gatsby's static-hosting speed advantage for that page.
Image Optimization with gatsby-plugin-image
`gatsby-plugin-image` (replacing the older `gatsby-image`) processes images at build time — resizing, format conversion (WebP/AVIF), lazy loading, and blur-up placeholders — and exposes them through the `gatsbyImageData` GraphQL resolver, avoiding hand-written `<img>` tags and layout shift.
import { StaticImage } from 'gatsby-plugin-image'
// StaticImage — for images whose path is known at build time, no GraphQL query needed
function Hero() {
return (
<StaticImage
src="../images/hero.jpg"
alt="Team working together"
layout="fullWidth"
placeholder="blurred"
formats={['auto', 'webp', 'avif']}
/>
)
}
// GatsbyImage — for images that come from a GraphQL query (CMS, dynamic content)
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
function ProductCard({ product }) {
const image = getImage(product.thumbnail)
return <GatsbyImage image={image} alt={product.name} />
}Common Gotchas
Expecting page queries to work in nested components — `graphql` page queries only run on page/template components exported by `gatsby-node.js` or in `src/pages/`; use `useStaticQuery` for non-page components like a Header.
Forgetting the data layer is build-time — a CMS content edit doesn't appear until the next build (or DSG regeneration on next request); this surprises people expecting live updates like a traditional SSR app.
Using `<img>` instead of gatsby-plugin-image — loses automatic resizing, lazy loading, and format negotiation; also a common cause of Cumulative Layout Shift regressions.
Build times ballooning with thousands of pages — pure SSG rebuilds every page on every deploy; switch large, rarely-changing sections to DSG instead of accepting ever-growing CI times.
Mutating gatsby-node context incorrectly — `context` values passed to `createPage` must match the `$variable` names in the page query exactly, or the query silently returns null data instead of erroring.