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 |
---|---|---|
|
| An array of queries, which will be run in parallel. |
|
| Tells Pump to query the Draft API, plus subscribe changes in real time. |
|
| A render function which receives the results of the queries. When |
|
| Pump will infer your |
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.
Using Pump (recommended)
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:
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.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“