Utilities to help you integrate BaseHub to your App faster, and in a type-safe manner. Check out the code in GitHub.
Install
Terminal
npm i basehub
pnpm i basehub
yarn add basehub
Query Content with the SDK
The basehub
package exposes a CLI generator that, when run, will generate a type-safe GraphQL client. We use GenQL to achieve this (read their docs). Some features:
Infers types from your BaseHub repository... meaning IDE autocompletion works great.
No dependency on graphql... meaning your bundle is more lightweight.
Works everywhere
fetch
is supported... meaning you can use it anywhere.
It’s important to note that, while this is our recommended way to query the BaseHub API within a JavaScript/TypeScript stack, this client does come with limitations and doesn’t expose the full power of GraphQL. If you need to leverage every GraphQL feature, or you’re using another programming language, you should use another client.
1. Configure
Set the required environment variables.
.env
BASEHUB_TOKEN=<your-read-token>
# the following are optional
BASEHUB_DRAFT=<true|false> # defaults to false
BASEHUB_REF=<branch-name|commit-id> # defaults to your default branch
2. Generate
Use the basehub
script to generate a type-safe SDK for your app.
Terminal
npx basehub
pnpm basehub
yarn basehub
⚠️ Important: Make sure you run the generator before your app's build step. A common pattern is to run it in your postinstall script.
package.json
{
"scripts": {
"postinstall": "basehub"
}
}
3. Integrate
Import it into your app and retrieve your data without worrying about type definitions.
app/page.tsx
import { basehub } from "basehub"
const Page = async () => {
const data = await basehub({ next: { revalidate: 30 }}).query({
__typename: true
})
return <pre>{JSON.stringify(data, null, 2)}</pre>
};
export default Page
---
import { basehub } from "basehub"
// query variables
let slugs = false
let toc = false
let wpm = 3
let filter
let first = 3
const data = await basehub().query({
__typename: true
})
---
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Website</title>
</head>
<body>
<pre>{JSON.stringify(data, null, 2)}</pre>
</body>
</html>
Fragmenting
Fragments let you construct sets of fields, and then include them in queries where you need to. — GraphQL Docs
With our generated SDK, we can leverage the satisfies
feature from TypeScript (available since v4.9) to create reusable fragments in our code. This is how it’s done:
example.tsx
import { basehub, FieldsSelection, FaqComponent, FaqComponentGenqlSelection } from 'basehub'
// object for our query
export const faqFragment = {
_id: true,
_title: true,
question: true,
answer: true
} satisfies FaqComponentGenqlSelection
// type to reuse across components
export type FaqFragment = FieldsSelection<
FaqComponent,
typeof faqFragment
>
// Example usage if using Next.js / RSC:
// 1. We define a component that uses the fragment
const Faq = ({ data }: { data: FaqFragment }) => {
// `data` has the correct type!
return (...)
}
// 2. We query BaseHub and render Faq
const Page = async () => {
const { homepage } = basehub({ next: {revalidate: 30 }}).query({
homepage: {
faqs: {
items: faqFragment
}
}
})
return homepage.faqs.items.map(item => <Faq data={item} key={item._id} />)
}
Choosing another output directory with --output
By default, basehub
will generate the SDK inside node_modules/basehub/dist/generated-client
. While this is a good default as it allows you to quickly get started, this approach modifies node_modules
which, depending on your setup, might result in IDE or build pipeline issues. If this happens, please report the issue!
Additionally, you might want to connect to more than one BaseHub Repository.
To solve this, basehub
supports an --output
argument that specifies the directory in which the SDK will be generated. You then can use this directory to import generated stuff. For example: running basehub --output .basehub
will generate the SDK in a new .basehub
directory in the root of your project. You can then import { basehub } from '../<path>/.basehub'
and use the SDK normally.
We recommend including the new --output
directory to .gitignore
, as these generated files are not precisely relevant to Git, but that's up to you and shouldn't affect the SDK's behavior.
Rendering Rich Text in React
The Delivery API can return your Rich Text Blocks’ data in multiple formats:
Plain Text, will ignore all formatting, media, and custom components, easy to render.
HTML, will ignore custom components, easy to render.
Markdown, will ignore custom components, needs a markdown to HTML parser to render.
JSON, comes with everything, but needs something that understand and processes it.
In the case of the JSON format, the response will be an AST based on the TipTap editor spec. Because of the complexities associated with processing this JSON format, we’ve built a React Component called <RichText />
that will help us render our Rich Text content. This is how it works:
app/page.tsx
import { Pump } from "basehub/react-pump"
import { RichText } from "basehub/react-rich-text"
const Page = async () => {
return (
<Pump
draft={draftMode().isEnabled}
next={{ revalidate: 60 }}
queries={[
{
homepage: {
subtitle: {
json: {
content: true,
},
},
},
},
]}
>
{async ([{ homepage }]) => {
"use server"
return <RichText>{homepage.subtitle.json.content}</RichText>
}}
</Pump>
)
}
export default Page
When using the <RichText />
component, you can simply pass the JSON content into it via children
, and it’ll get rendered. If you want to use a custom handler for a certain HTML node (imagine using Next.js’ <Image />
to render images), you’d use the components
prop.
app/page.tsx
import { Pump } from "basehub/react-pump"
import { RichText } from "basehub/react-rich-text"
import Image from "next/image"
const Page = async () => {
return (
<Pump
draft={draftMode().isEnabled}
next={{ revalidate: 60 }}
queries={[
{
homepage: {
subtitle: {
json: {
content: true,
},
},
},
},
]}
>
{async ([{ homepage }]) => {
"use server"
return (
<RichText
components={{
img: (props) => <Image {...props} />,
}}
>
{homepage.subtitle.json.content}
</RichText>
)
}}
</Pump>
)
}
export default Page
<RichText />
will return the HTML for each node of content, without any <div>
wrapping everything nor any styles. We recommend using something like Tailwind Typography for quick prose styling, or of course, writing your own CSS.
If you are using Custom Blocks in your Rich Text, you’ll need to add them to your query, and pass in the blocks
prop. Then, you’ll be able to set up the custom renderers for them (in a type-safe manner, by the way):
app/page.tsx
import { Pump } from "basehub/react-pump"
import { RichText } from "basehub/react-rich-text"
import Image from "next/image"
import { Callout, CodeSnippet } from './path-to/components'
const Page = async () => {
return (
<Pump
draft={draftMode().isEnabled}
next={{ revalidate: 60 }}
queries={[
{
homepage: {
subtitle: {
json: {
content: true,
blocks: {
__typename: true,
on_CalloutComponent: {
_id: true,
intent: true,
text: true,
},
on_CodeSnippetComponent: {
_id: true,
code: {
code: true,
language: true,
},
fileName: true,
},
}
}
},
},
},
]}
>
{async ([{ homepage }]) => {
"use server"
return (
<RichText
blocks={homepage.subtitle.json.blocks}
components={{
img: (props) => <Image {...props} />,
CalloutComponent: (props) => <Callout data={props}>,
CodeSnippetComponent: (props) => <CodeSnippet data={props}>,
}}
>
{homepage.subtitle.json.content}
</RichText>
)
}}
</Pump>
)
}
export default Page
We hope this removes a bit of friction from the sill tough task of rendering Rich Text data.
Fast Refresh with Pump (Next.js Only)
Pump is a React Server Component that enables a Fast Refresh-like experience for your content. When set up, Pump will subscribe to real time changes from your Repo, and so keep your UI up-to-date. This is ideal for previewing content before pushing it to production. You can use it like this:
app/page.tsx
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Page = () => {
return (
<Pump queries={[{ _sys: { id: true } }]} draft={draftMode().isEnabled}>
{async ([data]) => {
"use server" // needs to be a Server Action
// some notes
// 1. `data` is typesafe.
// 2. if draft === true, this will run every time you edit the content, in real time.
// 3. you can render nested Server Components (Client or Server) here, go nuts.
return (
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
)
}}
</Pump>
)
}
export default Page
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 |
FAQ
“Does it work with other JS frameworks?” Not at this moment. We only support Next.js, as it’s the only React framework that supports RSC (as far as we know).
“Why do I need to pass a Server Action?“ Because when
draft === true
, this function will be re-executed when content from BaseHub changes, and in order to pass a function from the server to the client, it needs to be a Server Action.“Does it have any performance impact in production?” When
draft === false
, there’s no performance impact. The server code forreact-pump
is just 2KB.