Dynamic Gatsby pages at load time with Sanity and URL parameters

118 replies
Last updated: Mar 19, 2021
Hello everyone 🙂
Using Gatsby, I need to load in content at
load time not build time which I agree goes against the idea of Gatsby being static content but the client has dropped this on me at the final hour and I don’t have much choice.
The current URL structure is like this:
<http://www.example.com/category/the-article-slug|www.example.com/category/the-article-slug>
but if you visit that, you get a 404 for
category/the-article-slug.js
— ideally here it would somehow load
article.js
and pass it the
/category/
and
/the-article-slug
to fetch with the sanity client
My other option was to change the URL structure like this:
<http://www.example.com/article?category=category&slug=the-article-slug|www.example.com/article?category=category&slug=the-article-slug>
but this loses some of its clean appeal and isn’t ideal
I posted this on Stack Overflow but the reply I got seemed to think it’s impossible

https://stackoverflow.com/questions/66699757/dynamic-pages-at-load-time-not-build-time
AI Update

I understand your predicament! You're trying to fetch Sanity content at runtime in Gatsby rather than at build time, while maintaining clean URLs like /category/the-article-slug. This is definitely possible, though it does go against Gatsby's typical static generation approach.

The matchPath Solution

The Stack Overflow response saying it's impossible is incorrect. Gatsby has a matchPath feature specifically for client-only routes that will solve your problem.

When you create your page in gatsby-node.js, specify a matchPath that matches your dynamic URL pattern:

// gatsby-node.js
exports.createPages = async ({ actions }) => {
  const { createPage } = actions
  
  createPage({
    path: '/article', // The actual page component path
    matchPath: '/:category/:slug', // This matches your URL pattern
    component: require.resolve('./src/templates/article.js'),
  })
}

Accessing Parameters and Fetching Content

In your article.js template, access the route parameters and fetch from Sanity at runtime:

// src/templates/article.js
import React, { useEffect, useState } from 'react'
import sanityClient from '@sanity/client'

const client = sanityClient({
  projectId: 'your-project-id',
  dataset: 'production',
  useCdn: false, // Use false for fresh data
  apiVersion: '2023-01-01'
})

const ArticlePage = ({ location }) => {
  const [content, setContent] = useState(null)
  
  useEffect(() => {
    // Extract category and slug from the URL
    const pathParts = location.pathname.split('/').filter(Boolean)
    const [category, slug] = pathParts
    
    // Fetch from Sanity at load time
    const query = `*[_type == "article" && category->slug.current == $category && slug.current == $slug][0]`
    
    client.fetch(query, { category, slug })
      .then(data => setContent(data))
      .catch(err => console.error(err))
  }, [location.pathname])
  
  if (!content) return <div>Loading...</div>
  
  return (
    <article>
      <h1>{content.title}</h1>
      {/* render your content */}
    </article>
  )
}

export default ArticlePage

How matchPath Works

The matchPath parameter tells Gatsby: "When someone visits any URL matching /:category/:slug, serve the article.js component and let it handle the routing client-side." This is exactly what you need - clean URLs with runtime content fetching.

Important Considerations

SEO Impact: Since content loads at runtime on the client, search engines won't see the content in the initial HTML. This is a significant SEO disadvantage compared to static generation.

Performance: You'll lose the performance benefits of pre-rendered pages, as each visit requires a client-side API call to Sanity.

Alternative Approaches (if you have any flexibility):

  • Gatsby DSG (Deferred Static Generation): Generate pages on first request, then cache them
  • Gatsby SSR: Server-side render pages on each request with full SEO benefits
  • Next.js with Sanity: Better support for dynamic content with ISR or SSR, maintaining both clean URLs and SEO

The matchPath approach will work perfectly for your /category/the-article-slug URL structure while fetching content at load time. This is a documented Gatsby feature designed exactly for client-only routes and SPA-like behavior within Gatsby sites.

Show original thread
118 replies
Would you not be able to use gatsby incremental builds?
You obviously loose the benefits of gatsby, but means some of your content could be dynamic whilst others rendered at build time.
If you look at the live site currently, it’s all done at build time: www.dscvrd.co but they’re saying the build times are too slow (I optimised the hell out of it and have them down to 1m30s but that’s still not fast enough) so it has to be ‘instant’ now
I’m going to keep a lot of the data at build time, but the articles and article content has to be at load time
Yeah so with gatsby you’d prefix those pages with a path and then use reach router to handle all those urls.
Is your client worried about seo?
I suppose they would be, yeah
I guess that’s going to be the other issue isn’t it — doing it this way is gonna mean these pages don’t ‘exist’ and therefore won’t be indexed?
They will be, google crawls using a mobile js enabled browser, but it does mean they’ll get hit by the google web vitals tax soon.
☝️ saved me a search, thanks
wait a sec.
I guess as long as they’re being indexed, that should be enough for me
What your client should be aware of is that lighthouse scores matter https://web.dev/measure/
On the fullscreen nav do you think there’s a way of stopping it jumping to the yellow on hover of nav items?
They’re coming from a WordPress/database powered website so these are more like issues I’ve created for them by switching to Gatsby 😕
How do you mean?
When i hover over those the yellow flashes up, but if it faded between that would be really nice.
Anyway back to your client.
1m30 is a really good build time.
Ha yeah I could add an animation to it for sure 🙂
I think so, but I suppose they’re going to add more and more pages, eventually that number will increase
I feel like the only way to do this is with the
article?cat=category?slug=example-slug
approach
No. You can do
/articles/slug
or
category/slug
Without getting a 404?
Yes.
You setup a page called category then use reach router to deal with the slug.
Did you take a look at the gatsby link?
I’m just reading it now
I missed it the first time
Also with gatsby incremental builds. it stores a cache so only builds the pages you need to update each time.
How are you building the site?
ci?
I have that, I upgraded to gatsby 3 recently
Netlify yeah
Yeah netlify supports that.
So it’s likely your 1m30 build is actually your baseline? where nothing is really changing.
Then they’ll be a slight overhead for each page that changes per build?
That adds up
What i’d do is a quick proof of concept for an article using reach-router.
Then profile it in lighthouse.
That’ll give you a sense of the value to the user for a longer build time.
and for your client improved seo and google rank.
Did you design it too?
Yeah it’d be nice to put both things forward. They were really p--’d off about the “long build time”, my fault as I guess I should have been more forthcoming about the approach but hey, we’re learning
Yeah I designed it too
A developer and designer. Good job.
Thanks Raffi 🙂
Coming from wordpress what was their cache times?
I don’t think they’d have that information
that was probably 1 minute or more.
Cache time would be the time from when a user visits the site, to revisiting the site and seeing fresh content from the database?
Well in wordpress they’ve probably got a cache somewhere.
on the server or cdn.
Did the gatsby link give enough info?
I haven’t used
@reach/router
before
So just getting to grips
Yeah this is what I’m reading 🙂
I’ve created a
pages/category.js
file and I would guess I need to put the router code here?
The page will be your path.
then:

import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/Layout"
import Article from "../components/Article"
import Listing from "../components/Listing"
const App = () => {
  return (
    <Layout>
      <Router basepath="/category">
        <Article path="/:slug" />
        <Listing path="/" />
      </Router>
    </Layout>
  )
}
export default App
and in your article component you can use: https://reactrouter.com/web/example/url-params
and this would be in
category.js
right? ☝️
If you want it to match
/category
then gatsby builds a placeholder page.
So what you are really doing is building a single page app in your gatsby built site.
each base path needs it’s own page / router.
What urls structures do you want to support?
Well from the live site, you can visit the category directly which gives you a list of articles within this category: https://dscvrd.co/noise
ah ok.
and then from that you can go to an article: https://dscvrd.co/noise/slowthai-misunderstood-or-making-trouble
you need it for wildcard base paths.
The category pages are created at build time which is fine, they rarely if ever add new categories
Ok. What you’ll need to do is a bit annoying then.
You need 1 page per category.
So you’ll probably want to create a template for a page then dynamically add the pages you need with a script before the gatsby build.
Ha yeah I thought as much, so
noise.js
format.js
etc?
Otherwise your root page will need to be dynamic too.
or could I not use my
category.js
TEMPLATE file for this?
Yeah you can do that.
your page context would add the basepath for the router.
I’m doing this which seems to work nicely:

const Category = ({ path }) => (
  <>
    <Router basepath={path}>
      <ArticlePage path="/:slug" />
      <CategoryListings path="/" />
    </Router>
  </>
);
Although, actually, the article pages return a 404
looking for
src/pages/noise/slowly-slowly-isolation-radio-station.js
Is this in the build step or the server?
Server. If I visit
/noise/
it picks up the category route
but visiting
/noise/some-slug
it 404's
  <Router basepath={path}>
    <ArticlePage path="/:slug" />
    <CategoryListings path="/" />
  </Router>
No 😓
That tells gatsby router to forward the path/* to the same page.
I guess I would swap
app
with
category
here?
yes.
oh sorry.
you want to do it per category.
So
noise
,
format
, etc?
// Implement the Gatsby API "onCreatePage". This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions
  // Loop through categories and do the following per category
  if (page.path.match(/^\/{CATEGORY_SLUG}/)) {
    // page.matchPath is a special key that's used for matching pages
    // with corresponding routes only on the client.
    page.matchPath = `/${CATEGORY_SLUG}/*`
    // Update the page.
    createPage(page)
  }
}
Where does CATEGORY_SLUG come from?
it’s psuedo code.
However you got the listing of categories for the page creation.
Ah
That’s handled in another function so might be tricky to pull it across. Maybe it’s easier to just include the category names in the regex as they’re pretty static
^\/(drop|format|kulture|noise|shred)\/$
just do that.
🙂
Feels hacky but this whole approach isn’t ideal
By the way thank you for being so helpful today, I’ve been pretty stressed out about this and you’ve really helped me out
Well if you wanted it dynamic you’d do a sanity query to pull the categories and do it that way.
Obviously still wouldn’t be
instant
True!
Nearly there with this approach 👌 if I can get the pages creating, I can pull all the data in tonight
Hope it all works out.
Thanks! I have the slug pulling in now, just need to run the groq query to fetch the data and we’re done 😄

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?