Querying Dynamic Content in Static Sites with GraphCMS and Gatsby

In the world of static site generators, one rule remains constant - the build is the gospel. Whatever was at build time will be until a new build occurs. But sometimes our data, you know, changes.

Jesse Martin
Jesse Martin
Dynamic Content with Gatsby and GraphCMS with a Headless CMS for Hotels and Travel

TLDR; Here's the important stuff.

?️ Repo: https://github.com/GraphCMS/example-gatsby-static-dynamic-hotel

⚡️ Demo: https://gatsby-static-dynamic-hotel.now.sh

IntroAnchor

In the world of static site generators, one rule remains constant - the build is the gospel. Whatever was at build time will be until a new build occurs. It's part of what makes them so fast, anything sourced from a database, flat-file, CMS, or what have you - becomes stamped into code in HTML, CSS, and JavaScript. Once that transformation occurs, there's no need for data fetching, transformation or template rendering - that part is done! Browsers can simply show you exactly what the server sends.

But sometimes our data, you know, changes. Imagine running a stock exchange on a static site?! Even modern ecommerce sites have pricing that can vary hundreds of times a day to reflect real-time price manipulation supply and demand forces.

So what's a JAMing developer to do? Well, the obvious solution is to build the parts that have a longer "TTL (time-to-live)" and fetch the changing bits from the client.

When dealing with Gatsby, however, that presents a challenge. One of my favorite things about Gatsby is the content mesh API that it creates. You can throw nearly any data source at it, even plain text files, and someone in the community will have created a plugin that parses the data and puts it into a flexible GraphQL API. From there, you can query all the data you want, and push the data through React templates. It's really a fantastic experience.

But it only works at build time. The API goes away, the data is stamped to a persistent state for rehydration, the clowns get back in the car and go home. If you want to query data from the browser, you'll need to reference back to the original data source that the Gatsby plugin you are using is sourcing from. Most external systems are still exposing a REST interface which means you now need to work with two different API protocols.

A Hero emergesAnchor

Thankfully, a growing number of online services are beginning to expose a native GraphQL interface, too! We at GraphCMS have been native GraphQL from the beginning, and when you source content from us, you can use the same knowledge base and experience you've gathered building the static site to now fetch content straight from the original source.

Let's look at the two places we fetch data in our demo example. Our domain, for context, is a hotel listing website, cleverly named "Gotell" that fetches available rooms dynamically.

macbook-pro-clay-dark-floating-to-the-left-squashed.png

To generate this index page, we fetch our data in the gatsby-node.js file. As I generated multiple demos for this talk, you'll notice I fetch TWO batches of data and merge them together, this is not needed in most cases but I chose to do that since I used the separate data sources for other demos.

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
// import various templates needed...
const minimalQuery = await graphql(
`
query {
gcms {
hotels {
id
slug
name
}
}
}
`
)
if (minimalQuery.errors) {
throw result.errors
}
const minHotels = minimalQuery.data.gcms.hotels
const extendedQuery = await graphql(
`
query {
gcms {
hotels {
id
description
photos {
url
}
}
}
}
`
)
if (extendedQuery.errors) {
throw result.errors
}
const extendedHotels = extendedQuery.data.gcms.hotels
// Create a merged data set, what would essentially be one large query"
const hotels = merge(minHotels, extendedHotels)
/*
Demo One!
Creating a single large index from the content
*/
createPage({
path: "/demo-one/hotels",
component: hotelIndexPage,
context: {
hotels,
},
})
...
})

We pass that list of hotels to the pageContext where it will be converted to HTML at build time. From the template, we fetch our dynamic content.

const Hotels = ({ pageContext }) => {
const [hotels, updateHotels] = useState(pageContext.hotels)
const [roomsFetched, setRoomsFetched] = useState(false)
useEffect(() => {
let isCurrent = true
;(async () => {
const { data } = await postData(process.env.GATSBY_GCMS_URL, {
query: `query {
hotels {
id
rooms
}
}`,
})
if (isCurrent) {
updateHotels(hs => merge(hs, data.hotels))
setRoomsFetched(true)
}
})()
return () => (isCurrent = false)
}, [])
return (
<Layout>
<SEO title="Demo One" />
<div className="flex flex-wrap">
{hotels.map((hotel, key) => (
<HotelBox hotel={hotel} key={key} roomsFetched={roomsFetched} />
))}
</div>
</Layout>
)
}

An important detail here is that we push the pageContext data into the React state and iterate over that array instead of directly from pageContext. This allows us to update our state with the fetched data from our useEffect hook and update the entries where applicable.

SummaryAnchor

There's nothing really more complex about this example. The basic process follows fetching data at build time, then fetching partial data at client load time and using an updater pattern to let us combine the data on the page. The key benefit here is being able to use GraphQl for both parts. If you examine the two code samples above we are writing nearly identical query syntax in both cases (our gatsby-source-graphql plugin adds a new top-level type which introduces an extra level of nesting for the build-time query.)

Removing the cognitively expensive context switching from any other API source to the data-mesh GraphQL API is a major win. The only catch is that you need a system that natively supports GraphQL - which is something we are happy to help you with!

It's Easy To Get Started

GraphCMS plans are flexibly suited to accommodate your growth. Get started for free, or request a demo to discuss larger projects with more complex needs