`basehub` SDK

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:

  1. Plain Text, will ignore all formatting, media, and custom components, easy to render.

  2. HTML, will ignore custom components, easy to render.

  3. Markdown, will ignore custom components, needs a markdown to HTML parser to render.

  4. 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

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.

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 for react-pump is just 2KB.