Data Fetching
This guide will focus on using GraphQL to fetch content from Contenful. The decision to use GraphQL as opposed to Contentful Delivery API is documented here and you should give it a look to get a general sense of what you need to expect, it covers limitations of both REST and GraphQL approaches and mitigations in place.
Fetching data with GraphQL is pretty simple, with it's introspective API you can explore the schema using tools like GraphiQL available as a Contentful app, or you can use your IDE/code editor if configured correctly, see guide
We'll focus on the fundamentals for now. We'll work over a few scenarios for data fetching during this guide, starting with the most common and simple, fetching static content.
Fetching static content
Static content, here defined as content that requires no user inputs and does not change based on application state, can be fetched ahead of time and pre-rendered in Next.js. We will assume this is 90% of the cases for marketing sites. Let's clarify with an example.
Let's say we're rendering the homepage, which consists of Header, Footer, SEO metadata, Page content (Topic in Contentful terms), and top and bottom components. Some of the elements are shared between pages, like layout elements (header, footer), while others are page-specific.
Layout elements are often stored in global configuration entries, like Header and Footer, and are shared between pages. Example hierarchy can look like this:
Content entries also come in hierarchies, for example a page Entry can include the following:
Ideally we want to describe one query for the layout (header, footer) and one for the page and it's components.
Layout query
For layout level queries, we recommend using RootLayout or the top-most shared layout between all pages and query all global elements directly with GraphQL in a single request.
Let's explore the demo content model, and how one can do top-level data fetching. The example content model has 3 content types related to navigation: Navigation Menu, Footer Menu and Menu Group, with Navigation Menu being used for header, while Footer menu being used for footer links.
To construct the header, we'll need to query Navigation Menu and the hierarchy of linked Menu Groups, let's see how it looks like:
To query the menu hierarchy we will construct a GraphQL query that looks like this:
You can notice we are getting all navigation menus entries, but since we know there is only one available, we're limiting it to 1.
Seems pretty straightforward, but let's expand our layout query to include footer menu as well.
Notice our layout has knowledge of our content types internals (page) and units of our content architecture, also you will find some repetition, this is not good.
For example a Page content type has pageName
and slug
fields, but we're exposing this implementation detail to our Layout query instead of sharing it across our application.
Same for Menu Group content available in menuItemsCollection fields on both header and footer menus, but also used in legalLinks.
We should be able to reuse some of this logic. This is what GraphQL Fragments are designed to solve.
Let's refactor our query to use fragments:
Here we added 2 new fragments, one for displaying page fields needed to show a link, and another MenuGroupFields fragment for rendering a menu group with nested PageLinkFields fragment. For now inline fragments will do, we will explore later more ways to reuse fragments and you'll notice our actual code has a better implementation with co-located fragments, but more on that in next section.
Now that we've constructed the query, you can fetch the data using graphqlClient (via urql) like so:
For our layout query to work, we need to pass it locale and preview variables. Those determine the locale to fetch for menu items and whether we should fetch draft content or not.
In this example we're using getLocaleFromPath
to get the Contentful locale name (e.g. en-US
) from the path (e.g. /en/
), and passing it to the query.
We also use isDraftMode
to determine if we should fetch draft content or not. It's coming from draftMode()
API from Next.js.
We're wrapping the query in graphql()
function call from gql.tada so that gql.tada can do it's magic, but we'll learn more about it in the next section.
Let's explore a page-level query now.
Page query
While layout query is being shared across all pages, for page-level queries we'll need to fetch data for a specific page, for this we'll need to know the page slug, which is controlled in Contentful. We'll use a dynamic route segment to implement dynamic routing for pages in our starter. This is organized the following way:
Let's break down this example
[[...slug]]
is a dynamic route segment which will respond to any path that is not defined in the static routes, including the root path, due to[[...slug]]
being an optional catch-all segment.[locale]
is a route segment that precedes the slug segment and will help us localize the page. Keep in mind that while it's not optional, you can tweak it to be optional using middleware rewritesgetPage
is a function that will fetch the page data from Contentful, we will define it in the next step
We won't look at ComponentRenderer in this guide, but it's a component that will render our components based on the data we fetch from Contentful. Read more about our component architecture here.
Let's build the query for getPage
function. I've ommited some fragments for brevity, but added enough to illustrate complexity of the query.
And now we can use that query in our getPage
function.
Those simple examples should illustrate the work that goes into data-fetching on both page and layout level, but we should unpack how we can make the query authoring, TypeScript inference and reusability better.
Query Authoring
I mentioned gql.tada in the previous section, but let's explore it in more detail a few benefits it provides.
Typescript inference
The biggest benefit of using GraphQL in any project would probably be the developer experience you can get with it. GraphQL being typed and having a schema, you can get autocomplete feature and type-safety in your IDE, with the right tooling.
For this starter we adopted gql.tada to implement typesafe data-fetching.
In a nutshell, gql.tada is a tool that will help us get typed GraphQL responses in Typescript by running a Typescript plugin in our IDE/editor of choise, and teaching Typescript how to infer types from our GraphQL schema, our queries and fragments.
Let's take our getPage funciton from the previous section and see how we can get autocomplete for the response, we'll model it without using gql.tada first to demonstrate the problem.
See how we have pageCollection
and pageData
inferred as any
? That's because we're sending effectively a string to urql
(we send a DocumentNode, but urql can only infer types from TypedDocumentNode)
If we'd like to get autocomplete and type-safety, we can use gql.tada's graphql()
function to get a TypedDocumentNode
query.
You can see how pageData variable is now typed with a proper shape of the response based on the query we sent, and you get autocomplete for the response if you start typing fields like pageData.slug
.
The graphql()
function from gql.tada is doing the magic here, it's parsing the query and fragments and generating a TypedDocumentNode
that can be used with urql, but it unlike some
alternative codegen solutions (like the popular client preset with graphql-codegen), it does that in IDE on the fly without the
need to run a codegen step and compute files with types.
There is a setup step to make it work though. Mainly you will need to have a graphql schema, and a configured Typescript GraphQLSP LSP plugin in your IDE. We'll explore both in the next subsections.
Regenerate graphql schema
If you're adding new content types or making changes to the content model, you will need to generate a new graphql schema to get type inference in Typescript working and to get autocomplete in IDE. This can be done by running:
After new types are generated, you will get changes in ./gql/
folder that you'll have to commit after you are done developing the feature.
Keep in mind, schema generation will take your .env.local
and read the CONTENTFUL_ENVIRONMENT you are pointing to, so if you create a new content type on a different environment,
it will not be pulled, or the opposite, if you have unwanted content types in your sandbox environment,
they will all appear in the schema. Make sure you commit changes you intend to commit!
Configure editor to use gql.tada and autocomplete
GraphQL tooling needed locally consists of 2 parts:
- GraphQL Language support
- Typescript inference with gql.tada
Autocomplete and language support:
Make sure if you're using an editor with a plugin system, you'll want to install a plugin with GraphQL language support. In VSCode or Cursor you can use GraphQL: Language Feature Support. In Webstorm you can use GraphQL Plugin Those plugins/extensions typically will load GraphQL config from any GraphQL Config file, mainly we use .graphqlrc.yml and enable features such as:
- Query syntax highlighting
- Autocompletion of queries
- Go to definition
If you need the plugin/extension to extract queries/fragments from more places, make sure to look at .graphqlrc.yml rules, as those define what files get scanned for GraphQL queries and fragments.
[!WARNING] Keep in mind that if you're using VSCode or Cursor and you have issues with "Unknown fragment" errors, you'll want to downgrade GraphQL Language Feature Support extension to version 0.9.3 until this issue is resolved.
gql.tada support
While gql.tada comes pre-configured with the project, you might want to know a little about how it works. You can refer to workflows docs to learn more about how to use gql.tada, but we'll give you the gist here.
In order for gql.tada's GraphQLSP plugin to work (and infer types), you need to have local Typescript server making the type inference (node_modules/typescript/lib
)
If you are using VSCode, the .vscode/settings.json in the repo is already configured to use the local Typescript server, but if things don't work, please double-check.
If using Webstorm, make sure you configure the Typescript interpreter from node_modules/typescript/lib as well under Settings -> Language & Frameworks -> Typescript
If you can't use a Typescript server in your IDE, you can optionally generate a gql/graphql-env.d.ts by running this command:
This command will also run gql.tada turbo
which will generate a cache file that should also be commited. This cache file will speed up inference for new users who just checked out a new branch.
More info here