Next.js

Connecting BaseHub to Next.js is very simple. First of all, you’ll need to have the basehub SDK installed and configured. Refer to our guide to do so.

Fast Refresh with Pump

Pump is a React Server Component that enables a Fast Refresh-like experience for your content. When draft === true, Pump will subscribe to changes in real time from your Repo, and so keep your UI up-to-date. This is ideal for previewing content before pushing it to production. When draft === false, Pump will hit the Query API directly, without any production impact whatsoever. You can use it like this:

app/page.tsx

import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"

const Page = async () => {
  return (
    <Pump
      next={{ revalidate: 30 }}
      draft={draftMode().isEnabled}
      queries={[{ __typename: true }]}
    >
      {async ([data]) => {
        "use server"
        return <pre>{JSON.stringify(data, null, 2)}</pre>
      }}
    </Pump>
  )
}

export default Page

Pump receives the following props:

Key

Type

Description

queries

Array<QueryGenqlSelection> (a valid genql query)

An array of queries, which will be run in parallel.

draft

boolean

Tells Pump to query the Draft API, plus subscribe changes in real time.

children

(data: QueryResults) => Promise<React.ReactNode>

A render function which receives the results of the queries. When draft === true, it’ll be passed to the Client as a Server Action.

token?

undefined | string

Pump will infer your BASEHUB_TOKEN from your environment variables. You can use this to override that.

Querying Content Outside of JSX

While Pump works great for querying and rendering content in JSX, there are times when you need to query content outside of the context of a component. For example, in Next.js’ generateStaticParams, or in generateMetadata, or in any other place you need, you’ll want to use basehub() directly.

This is how you can do it:

app/page.tsx

import { basehub } from "basehub"
import { draftMode } from "next/headers"
import { Metadata } from "next"

export const generateMetadata = async (): Promise<Metadata> => {
  const data = await basehub({
    next: { revalidate: 30 },
    draft: draftMode.isEnabled(),
  }).query({
    __typename: true, 
  }) 
}

const Page = async () => {
  return <div>{...}</div>
}

export default Page

Caching, and Revalidation

By default, Next.js will try to cache all of our requests made with fetch—and that includes BaseHub. While this makes subsequent requests to BaseHub much faster, it’ll essentially make your website’s content fully static, instead of reflecting the latest content changes from your BaseHub Repo.

app/page.tsx

import { Pump } from "basehub/react-pump"

const Page = async () => {
  return (
    <Pump queries={[{ __typename: true }]}>
      {async ([data]) => {
        "use server"
        // `data` will be cached by Next.js

        return <pre>{JSON.stringify(data, null, 2)}</pre>
      }}
    </Pump>
  )
}

export default Page

Our recommendation is to use their time-based revalidate caching option, so that your data is cached and fast, but it also reacts to new content coming from BaseHub.

app/page.tsx

import { Pump } from "basehub/react-pump"

const Page = async () => {
  return (
    <Pump
      next={{ revalidate: 30 }}
      queries={[{ __typename: true }]}
    >
      {async ([data]) => {
        "use server"
        // `data` will be stale after 30 seconds

        return <pre>{JSON.stringify(data, null, 2)}</pre>
      }}
    </Pump>
  )
}

export default Page

You can pass any fetch options that Next.js exposes, such as cache, and next to configure caching for your use case.

On-demand Revalidation

You can leverage BaseHub Webhooks and Next.js On-Demand Revalidation to update the cache of your Next.js Apps in a more fine-grained fashion. Instead of checking to see if something changed every n seconds, you can listen to the repo.commit event from BaseHub and use revalidateTag or revalidatePath to revalidate on demand.

Let’s set it up!

1. Set up API Endpoint that will revalidateTag

/app/api/revalidate-basehub/route.ts

import { revalidateTag } from "next/cache"

const webhookSecret = process.env.BASEHUB_WEBHOOK_SECRET
if (typeof webhookSecret !== "string") {
  throw new Error("Missing BASEHUB_WEBHOOK_SECRET.")
}

export const POST = (request: Request) => {
  /**
   * Authenticate the request.
   * For more security, follow the Svix guide on how to verify a webhook: https://docs.svix.com/receiving/verifying-payloads/how
   * For simplicity, and because revalidating a cache is not a security risk, we just do basic auth
   * with a Bearer token we'll set up ourselves.
   */
  const authorization = request.headers.get("authorization")
  if (authorization !== `Bearer ${webhookSecret}`) {
    return Response.json({ message: "Unauthorized." }, { status: 401 })
  }
  revalidateTag("basehub") 
  return Response.json({ revalidated: true, now: Date.now() })
}

The BASEHUB_WEBHOOK_SECRET can be whatever you define it to be. Just make sure you pass it via the Authorization header. You can generate a random password using the 1Password generator.

2. Make sure your queries have the Cache Tag you want to revalidate

lib/basehub-client.ts

import { Pump } from "basehub/react-pump"

const Page = async () => {
  return (
    <Pump
      next={{ tags: ["basehub"] }}
      queries={[{ __typename: true }]}
    >
      {async ([data]) => {
        "use server"

        return <pre>{JSON.stringify(data, null, 2)}</pre>
      }}
    </Pump>
  )
}

export default Page

3. Create the Webhook

To do this final step, follow our guide on setting up Webhooks via the Webhook Portal.

Previewing Content

To set up a preview workflow with BaseHub and Next.js, you’ll need to first configure Draft Mode in Next.js. If you use Vercel, the Vercel Toolbar will come with Draft Mode already configured, which is great.

To get draft content, we simply need to pass draft={true} to our Pump component. It’s recommended to do so conditionally, depending on the state of Next.js’ Draft Mode:

app/basehub/queries.ts

import { basehub } from "basehub"
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"

const Page = async () => {
  return (
    <Pump
      next={{ revalidate: 30 }}
      draft={draftMode().isEnabled}
      queries={[{ __typename: true }]}
    >
      {async ([data]) => {
        "use server"
        return <pre>{JSON.stringify(data, null, 2)}</pre>
      }}
    </Pump>
  )
}

export default Page

Using basehub().query()

If you’re using basehub() directly, you can also pass a draft param::

app/basehub/queries.ts

import { basehub } from "basehub"
import { draftMode } from "next/headers"
import { Metadata } from "next"

export const generateMetadata = async (): Promise<Metadata> => {
  const data = await basehub({
    next: { revalidate: 30 },
    draft: draftMode().isEnabled, 
  }).query({
    __typename: true,
  })
}

const Page = async () => {
  return <div>{...}</div>
}

export default Page

Example: Creating a Blog

Imagine you're creating a Blog. This would be a typical file structure:

app/
├── blog/
│   ├── page.tsx
│   └── [slug]/
│       └── page.tsx
next.config.js
package.json
... more files

This is how our app/blog/page.tsx could look like:

app/blog/page.tsx

import { basehub } from "basehub"
import { Pump } from "basehub/react-pump"
import { RichText } from "basehub/react-rich-text"
import Link from "next/link"
import type { Metadata } from "next"
import { draftMode } from "next/headers"

export const revalidate = 60
export const dynamic = "force-static"

export async function generateMetadata(): Promise<Metadata> {
  const { blog } = await basehub({
    next: { revalidate: 60 },
    draft: draftMode().isEnabled,
  }).query({
    blog: { meta: { title: true, description: true, ogImage: { url: true } } },
  })

  return {
    title: blog.meta.title,
    description: blog.meta.description,
    // etc...
  }
}

const BlogPage = async () => {
  return (
    <Pump
      next={{ revalidate: 60 }}
      draft={draftMode().isEnabled}
      queries={[
        {
          blog: {
            header: { title: true, subtitle: { json: { content: true } } },
            posts: {
              __args: { first: 10, orderBy: "publishDate__DESC" },
              items: {
                _id: true,
                _title: true,
                _slug: true,
                subtitle: true,
                publishDate: true,
                // more things
              },
            },
          },
        },
      ]}
    >
      {async ([{ blog }]) => {
        "use server"

        return (
          <div>
            <h1>{blog.header.title}</h1>
            <div>
              <RichText>{blog.header.subtitle.json.content}</RichText>
            </div>
            <ul>
              {blog.posts.items.map((post) => {
                return (
                  <li key={post._id}>
                    <Link href={`/blog/${post._slug}`}>{post._title}</Link>
                  </li>
                )
              })}
            </ul>
          </div>
        )
      }}
    </Pump>
  )
}

export default BlogPage

And then, our app/blog/[slug]/page.tsx could be something like the following:

app/blog/[slug]/page.tsx

import { QueryGenqlSelection, basehub } from "basehub"
import { Pump } from "basehub/react-pump"
import { RichText } from "basehub/react-rich-text"
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import { draftMode } from "next/headers"

export async function generateStaticParams() {
  const {
    blog: { posts },
  } = await basehub({ cache: "no-store" }).query({
    blog: { posts: { items: { _slug: true } } },
  })

  return posts.items.map((post) => ({ slug: post._slug }))
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const { blog } = await basehub({
    next: { revalidate: 60 },
    draft: draftMode().isEnabled,
  }).query(postBySlugQuery(params.slug))
  const [post] = blog.posts.items
  if (!post) notFound()

  return {
    title: post.meta.title,
    description: post.meta.description,
    // etc...
  }
}

const BlogPage = async ({ params }: { params: { slug: string } }) => {
  return (
    <Pump
      next={{ revalidate: 60 }}
      draft={draftMode().isEnabled}
      queries={[postBySlugQuery(params.slug)]}
    >
      {async ([{ blog }]) => {
        const [post] = blog.posts.items
        if (!post) notFound()

        return (
          <div>
            <h1>{post._title}</h1>
            <p>Published at: {new Date(post.publishDate).toLocaleString()}</p>
            <RichText>{post.content.json.content}</RichText>
          </div>
        )
      }}
    </Pump>
  )
}

const postBySlugQuery = (slug: string) => {
  return {
    blog: {
      posts: {
        __args: { first: 1, filter: { _sys_slug: { eq: slug } } },
        items: {
          _id: true,
          _title: true,
          publishDate: true,
          content: { json: { content: true } },
          meta: { title: true, description: true },
          coverImage: { url: true },
        },
      },
    },
  } satisfies QueryGenqlSelection
}

export default BlogPage

We’re hoping to release more full, real-world examples soon. Join the Community Discord to ask our community for specific use cases!


Troubleshooting

“Error: type 'Query' does not have a field 'blogIndex' “

This error may pop up when using basehub or Pump sometimes, and it could be due to two reasons:

  1. You altered your schema in BaseHub and didn’t run the generator again. Make sure to run basehub every time you alter your schema, so that we can re-infer it.

  2. You already ran the generator, but Next.js cached the previous generated code in the .next directory (this is a directory Next.js uses to store compiled code while running the dev server). To fix this, just delete the .next directory and start your project again.

TypeScript not autocompleting after re-generating the schema

This gets fixed by restarting the TS server. In VSCode, you can do:

  • Mac: Cmd + Shift + P + “Restart TS Server“

  • Win: Ctrl + Shift + P + “Restart TS Server“