BaseHub is a fast and collaborative Headless CMS.
Welcome to BaseHub Docs. Throughout this site, you’ll find instructions on how to connect BaseHub to your website using our SDKs, and you’ll learn more about our platform and features.
Jump straight into one of our most popular sections.
Have a specific question or support request? These are our support channels:
Discord: https://discord.gg/6Gk4qfuqHK
Help Center (live chat): https://basehub.com/help?chat=true
Understand the basics of the BaseHub Platform.
BaseHub has three main properties:
The Dashboard, basehub.com
, where you create teams, repositories, collaborate on content, etc
The GraphQ: API, api.basehub.com
, where you interact with your repository programmatically, either to query data in your repo, or to mutate data in your repo
The SDK, which you install and run within your website: pnpm i basehub
As you use BaseHub, you—or your team as a whole—will interact with all of these parts, and that’s why having a good understanding of the whole is important.
Every piece of content you create in BaseHub is a Block. Similar to Lego, Blocks can have different types and functions. You can nest Blocks, reference Blocks, and more.
In the Editor, you’ll create Blocks by typing /
and choosing one Block type from the Block selector.
Read more about Blocks in our Blocks Reference:
A Commit stores a snapshot of your Repo at that specific point in time. Inspired by Git, each commit is immutable, and it’s a core of how version control works in BaseHub.
Once you’re happy with your changes, you can create a Commit. The API will now use the latest commit (the Head Commit) to resolve your queries.
A great way to explore the GraphQL API is to use the Explorer. You can find it in the README:
Learn how to integrate your Next.js App with BaseHub in a couple of steps.
basehub
Our official JavaScript/TypeScript library exposes a CLI generator that, when run, will generate a type-safe GraphQL client. Check out our API Reference for more information.
Install with your preferred package manager.
npm i basehub
BASEHUB_TOKEN
Environment Variable2Get it from your BaseHub Repo’s README.
BASEHUB_TOKEN="<your-token>"
# Remember to also add this ^ env var in your deployment platform
In order to generate the BaseHub SDK, we recommend running basehub dev
in parallel to running the development server, and basehub
right before building the app.
"scripts": {
"dev": "basehub dev & next dev",
"build": "basehub && next build",
"start": "next start",
"lint": "next lint"
},
Using Windows? You might need to use something like concurrently
instead of using the &
to run a parallel node process. So:
concurrently \”basehub dev\” \”next dev\”
Give it a go to make sure the set up went correctly.
npm run dev
Now, let’s go ahead and query some content!
The recommended way to query content from BaseHub is with <Pump />
, a React Server Component that enables a Fast Refresh-like experience.
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Page = () => {
return (
<Pump
queries={[{ _sys: { id: true } }]}
draft={draftMode().isEnabled}
next={{ revalidate: 30 }}
>
{async ([data]) => {
"use server"
return (
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
)
}}
</Pump>
)
}
export default Page
Notice we’re using Next.js’ draftMode
and passing it down to Pump. You’ll learn more in the next section, but put briefly: 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.
Learn how to build GraphQL queries with the generated client.
When you run basehub
, you’ll be generating a GraphQL Client. What’s unique about this GraphQL client is that you’ll be defining the queries within your .{js,ts}
files, instead of within .graphql
ones. Most importantly, the output of your queries will be fully type safe.
Under the hood, we use https://genql.dev/, so make sure you check out that project out. If you want to see how a GraphQL query converts to a GenQL query, you can check out their converter tool.
basehub()
This function let’s you fire off a single, direct query. Because of this, it’s perfect for querying content that you don’t need to render, like when defining generateStaticParams
within a dynamic Next.js page.
import { basehub } from "basehub"
export const generateStaticParams = async () => {
const { posts } = await basehub({ cache: "no-store" }).query({
posts: { items: { _slug: true } },
})
return posts.items.map((p) => {
return { slug: p._slug }
})
}
<Pump />
Pump can subscribe to realtime changes from your dashboard, and re-compute the JSX so that you can have a Fast Refresh-like experience. Because of this, Pump is ideal for querying content that you’ll want to render—for example, titles, images, rich texts, etc.
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Page = () => {
return (
<Pump
queries={[{ _sys: { id: true } }]}
draft={draftMode().isEnabled}
next={{ revalidate: 30 }}
>
{async ([data]) => {
"use server"
return (
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
)
}}
</Pump>
)
}
export default Page
Under the hood, Pump uses basehub() as its client. You can think of Pump is an abstraction over the more primitive basehub(), that helps you get that realtime editing experience.
With these two ways of querying in mind, let’s explore how to build our queries.
Queries are JavaScript objects, where each key represents a key in the GraphQL schema, and the value is a boolean which decides weather you want to retrieve that key or not. Let’s take this query as an example:
import { basehub } from "basehub"
basehub().query({
_sys: {
id: true,
},
homepage: {
title: true,
},
posts: {
items: {
_id: true,
_slug: true,
_title: true,
publishedAt: true,
},
},
})
This query above will return the following result:
query {
_sys {
id
}
homepage {
title
}
posts {
items {
_id
_slug
_title
publishedAt
}
}
}
As you can see, GraphQL and TypeScript are not that different, and this is what our client is taking advantage of.
You can pass down arguments with __args
:
import { basehub } from "basehub"
basehub().query({
posts: {
__args: {
filter: {
_sys_slug: { eq: "my-post-slug" },
},
},
items: {
_id: true,
_slug: true,
_title: true,
publishedAt: true,
},
},
})
Fragments are very useful to define data dependencies inside your application. To define a fragment with our SDK, you’ll use fragmentOn
:
import { basehub, fragmentOn } from "basehub"
export const PostFragment = fragmentOn("PostItem", {
_id: true,
_slug: true,
_title: true,
publishedAt: true,
})
// you can use it as a type as well
export type PostFragment = fragmentOn.infer<typeof PostFragment>
basehub().query({
posts: {
__args: {
filter: {
_sys_slug: { eq: "my-post-slug" },
},
},
items: {
_id: true,
_slug: true,
_title: true,
publishedAt: true,
...PostFragment
},
},
})
A common pattern we enjoy using revolves around components defining thier own data dependencies. This works great with fragments, as we can easily define a fragment alongside a component and have it all be type safe.
// Let's imagine a Callout component:
import { fragmentOn } from ".basehub"
import { RichText } from ".basehub/react-rich-text"
export const CalloutFragment = fragmentOn("CalloutComponent", {
_id: true,
emoji: true,
body: { json: { content: true } },
})
export const Callout = ({
data,
}: {
data: fragmentOn.infer<typeof CalloutFragment>
}) => {
return (
<div>
<span>{data.emoji}</span>
<RichText>{data.body.json.content}</RichText>
</div>
)
}
Then you could use this CalloutFragment
paired with <Callout />
, all type safe and with the data dependency co-located. If you update your Callout component and require more data from BaseHub, you can update the fragment and you’ll instantly get the data coming via props.
Aliases are a very useful GraphQL feature, which unfortunately is not currently supported. If you need this feature, contact us to help us prioritize.
Fragments let you construct sets of fields, and then include them in queries where you need to.
The GraphQL 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:
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.
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 them via the blocks
prop. Then, you’ll be able to set up the custom renderers for them (in a type-safe manner, by the way):
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.
Understand the different environments and caching strategies you can leverage to improve your content editing experience.
Setting up your application so that it handles all of the environments in a seamless fashion is a very important part of integrating with BaseHub.
When developing in localhost, you’ll be writing new code and iterating over your content. This means adding new blocks, changing those blocks’ constraints, writing content and more—all at the same time.
In order to not break your flow, you’ll want two things:
For the schema in BaseHub to be in sync with your IDE, and
For the content to update as you write, without needing to commit it yet.
We bundled these two needs into one command:
basehub dev
This command generates the type-safe SDK and keeps it in sync with changes you make in basehub.com (this is called --watch
mode); and also sets up the SDK so that it queries Draft content from your repository.
This is why we recommend you run it in parallel to next dev
.
"scripts": {
"dev": "basehub dev & next dev",
"build": "basehub && next build",
"start": "next start",
"lint": "next lint"
},
Notice the single &
.
Setting up an easy way for editors to preview content before committing it into production is essential. We’ve designed our preview workflow with these three pillars in mind:
Content should render in real time, as you write.
Preview should be easy for developers to integrate.
The integration should never degrade production performance in any way.
We achieve this is by using a couple of BaseHub components, <Pump />
and <Toolbar />
, in combination to Next.js’ draftMode
. Additionally, to bridge the gap between basehub.com (the dashboard) and your website, you’ll need to set up the “Preview” Button.
This is how a simple code example can look like:
import { Toolbar } from 'basehub/next-toolbar'
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
<Toolbar />
</body>
</html>
)
}
Finally, we need to set up our Preview Buttons.
Once we’ve set up Local and Preview environments, most of the hard work is done. The only thing you need to make sure when going to production is for the SDK to be generated before the build step of your application.
"scripts": {
"dev": "basehub dev & next dev",
"build": "basehub && next build",
"start": "next start",
"lint": "next lint"
},
That should be it. You’re ready to deploy your website.
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.
This introduces a new task for the developer, which is to purge that cache when content from BaseHub changes. These are some of the options you have:
The absolute best method of revalidation is “on-demand”. As its name implies, it consists of purging the cached data at the exact moment a change occurs. This provides the best experience for editors, as they won’t need to refresh the website for several seconds to see their content live; and also keeps server costs down, as the server itself won’t need to constantly check with our API to see if something has changed.
BaseHub provides automatic on-demand revalidation in a fine-grained manner.
Automatic: without the need of constant developer setup.
Fine-grained: with every query being revalidated individually—in contrast to an “all or nothing” approach.
This is how:
layout.tsx
1This will add a Server Action to revalidate the specific tags basehub()
will set to each query.
// app/layout.tsx
import { Toolbar } from "basehub/next-toolbar"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
<Toolbar />
</body>
</html>
)
}
This will help our servers know where to go to to revalidate the queries.
That should be all! Make sure you don’t pass other cache-related props (such as revalidate
or cache
), as that will opt the query out of automatic on-demand revalidation.
As you may notice, we’re also not passing draftMode().isEnabled
via props, as this is no longer required as of basehub@7.5.10
—we now automatically infer draft mode for you.
import { Pump } from "basehub/react-pump"
import { basehub } from "basehub"
const Page = async () => {
// works with basehub and with Pump
const data = await basehub().query({ __typename: true })
return (
<Pump queries={[{ __typename: true }]}>
{async ([data]) => {
"use server"
return <pre>{JSON.stringify(data, null, 2)}</pre>
}}
</Pump>
)
}
export default Page
Wondering how does this all work? When basehub().query
is ran, we hash the query being sent and use it as a cache tag. We send this cache tag to our servers (alongside the query itself).
The server now runs the query and computes the response. It will then hash the response, and store the cache tag, the original query, and the response hash in our database.
On commit, we’ll get all of the queries we’ve been collecting and run them again against the newly committed tree of blocks. Now, one by one, we run them, compute the response hash, and compare it against the one we previously returned to the user. If response hashes don’t match, we need to revalidate the query.
To revalidate the query, we spin up a headless browser that navigates to your Website URL and executes the Server Action our <Toolbar />
created.
Read the full writeup in our blog to learn more.
Another conventional way to revalidate content is to use Next.js’ time-based revalidate
caching option.
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
While this is very easy to set up, automatic on-demand revalidation is always better, as editors won’t need to refresh the website for several seconds to see their content live; will keep server costs down, as the server itself won’t need to constantly check with our API to see if something has changed; and will simply remove one task from developers’ hands.
Learn how to add instant-search into your website.
BaseHub Search will help you create instant-search experiences ala Algolia inside your website. There are two steps into building an awesome search experience:
Indexing the content
Building the frontend
BaseHub helps you with both of these tasks.
BaseHub Search supports indexing blocks that are below Components. By default, blocks won’t be indexed. To index a block, you’ll go to over to its Properties Panel and click on the “Index block“ toggle.
The “Index block” state is set in a Component, and will be inherited by the Instances. Make sure you go to a Component to toggle this state.
In order for results to appear on your frontend, you’ll need to commit your changes. Toggling the “Index block” state is a “committable change.” Then, as you edit and add new content content, and commit, BaseHub Search will re-index and then your frontend will show the updated results.
There are a couple of ways in which BaseHub helps you build your search frontend. First, you’ll needit exposes a useSearch
hook to make the queries and manage state. Second, it exposes a <SearchBox>
component that provides good UX defaults for you to render your search input, your results, and your highlights.
In order for all of this to work, you’ll first need to get a _searchKey
from the GraphQL API, which is scoped to a specific Collection or Component.
Let’s imagine you have a Posts Collection and you want to build a frontend so search through it.
_searchKey
1This is how you’ll get your _searcKey
:
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Page = async () => {
return (
<Pump
next={{ revalidate: 30 }}
draft={draftMode().isEnabled}
queries={[
{
posts: {
_searchKey: true,
items: {
_id: true,
_title: true,
// more post stuff...
},
},
},
]}
>
{async ([data]) => {
"use server"
return (
<>
<Search _searchKey={data.posts._searchKey} />
...
</>
)
}}
</Pump>
)
}
export default Page
<Search>
component2You’re now ready to useSearch
and <SearchBox>
to build your UI.
'use client'
import { useSearch, SearchBox } from "basehub/react-search"
export const Search = ({
_searchKey,
}: {
_searchKey: string
}) => {
const search = useSearch({
_searchKey,
queryBy: ["_title", "body", "excerpt"],
})
return (
<SearchBox.Root search={search}>
<SearchBox.Input />
<SearchBox.Placeholder>
Start typing to search.
</SearchBox.Placeholder>
<SearchBox.Empty>
Nothing found for <b>{search.query}</b>
</SearchBox.Empty>
<SearchBox.HitList>
{search.result?.hits.map((hit) => {
return (
<SearchBox.HitItem
key={hit._key}
hit={hit}
href={`/blog/${hit.document._slug}`}
>
<SearchBox.HitSnippet fieldPath="_title" />
<SearchBox.HitSnippet
fieldPath="body"
fallbackFieldPaths={["excerpt"]}
/>
</SearchBox.HitItem>
)
})}
</SearchBox.HitList>
</SearchBox.Root>
)
}
Simple, non real-world example
GitHub Repo: https://github.com/basehub-ai/nextjs-simple-search-example
BaseHub Repo: https://basehub.com/basehub/simple-search-example
Advanced example (the search that powers these docs)
GitHub Repo: https://github.com/basehub-ai/nextjs-docs
BaseHub Repo: https://basehub.com/basehub/docs
Learn how to send analytics events from your website.
BaseHub’s Event Block provides a powerful way to know more about how your content is performing. The unique thing about it is that Events that occur throughout your website can be tied directly to a Block—therefore keeping it in context.
While this article focuses on analytics, the Event block is versatile and offers much more functionality, including two different layouts and multiple use cases. For a more complex use case, check out Forms.
The Event Block time-series can be used for tracking things like:
Page views (internal and user facing)
Button/Link Clicks
Feedback forms
Or anything you want, really.
First of all, you’ll need to add a new Event Block to your repo.
Create a new event block
Switch to Time-series layout
Commit your changes (not necessary if you’re working on draft mode)
Get your event’s ingestKey
ingestKey
vs adminKey
Keep in mind that the Event block exposes two different keys for different type of actions.
Since sending data is the most common, and at the same time the most safe, action in events, it has a distinctive ingestKey
that can be safely used client side.
On the other side, update and read access is reserved for the adminKey
and could be the case that the data stored being sensitive enough to protect that key and only use it server-side.
In order to send an event, you’ll need to first get the ingestKey
of an Event Block from the GraphQL API. Let's look at an example that tracks page views on the homepage. Once we get the page data and its ingestKey
, we’ll import { sendEvent } from "basehub/events"
and run it on mount:
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Homepage = () => {
return (
<Pump
next={{ revalidate: 30 }}
draft={draftMode().isEnabled}
queries={[
{
homepage: {
_title: true,
pageViews: {
ingestKey: true,
}
},
},
]}
>
{async ([{ homepage }]) => {
"use server"
return (
<div>
<PageView ingestKey={homepage.pageViews.ingestKey} />
<h1>{homepage._title}</h1>
</div>
)
}}
</Pump>
)
}
In case you want to show the Event Count in your website—for example, to render a “view count”—, well, you can! Following up from the <PageView />
component we built previously, we can update it so that it runs getEvents
and renders it:
"use client"
import * as React from "react"
import type { PageViews as PageViewsType } from "~/.basehub/schema"
import { sendEvent, getEvents } from "basehub/events"
export const PageView = ({
ingestKey,
adminKey,
}: {
ingestKey: PageViewsType["ingestKey"],
adminKey: PageViewsType["adminKey"]
}) => {
const [count, setCount] = React.useState()
// On mount, send the event
React.useEffect(() => {
sendEvent({ ingestKey, name: "page_view" })
}, [])
// We also get the event count so we can render it
React.useEffect(() => {
getEvents(key, {
type: 'time-series',
range: 'all-time'
}).then(
(response) => {
if (response.success) {
setCount(response.data)
}
},
)
}, [])
return <div>Views: {count ?? "Loading..."}</div>
}
The powerful Event block lets you build a form schema from the dashboard, and consume it in code to build complex forms.
Take our marketing website “Request a demo” page for example:
We can retrieve the fields from the schema and render them like this:
import { Button } from "@/common/button"
import {
AuthLayout,
RichTextFormWrapper,
formWrapperFragment,
} from "../_components/auth-layout"
import { Pump } from "basehub/react-pump"
import { ArrowRightIcon } from "@radix-ui/react-icons"
import { BackToHomeButton } from "../_components/back-to-home-button"
import { DemoForm } from "./client-form"
export default function RequestDemo() {
return (
<Pump
queries={[
{
site: {
requestDemo: {
wrapper: {
title: true,
subtitle: {
json: {
content: true,
},
},
cta: buttonFragment,
},
submissions: {
ingestKey: true,
schema: true,
},
},
},
},
]}
>
{async ([{ site }]) => {
"use server"
return (
<AuthLayout
subtitle={
site.requestDemo.wrapper.subtitle ? (
<RichTextFormWrapper>
{
site.requestDemo.wrapper.subtitle.json
.content
}
</RichTextFormWrapper>
) : null
}
title={site.requestDemo.wrapper.title}
>
<DemoForm
ingestKey={site.requestDemo.submissions.ingestKey}
schema={site.requestDemo.submissions.schema}
>
<div className="mt-3 flex items-center justify-between">
<Button
icon={<ArrowRightIcon className="size-5" />}
iconSide="right"
intent={site.requestDemo.wrapper.cta.type}
type="submit"
>
{site.requestDemo.wrapper.cta.label}
</Button>
<BackToHomeButton />
</div>
</DemoForm>
</AuthLayout>
)
}}
</Pump>
)
}
Learn how to use Webhooks to subscribe to changes that happen within BaseHub.
Workflows allow you to receive event notifications from BaseHub. BaseHub will send a POST request to a URL you specify when certain events happen in a BaseHub Repository.
A commit happens. – This is a useful notification that can help you set up on-demand revalidation for your Next.js Apps, amongst other things.
Collection Events: Row created, updated or deleted.
New events in Event Block.
To configure webhooks, you’ll need to create a new Workflow block in your repo. There you can setup the URL that will be requested on new commits. Make sure to commit the block to make it effective.
Learn how to add localization, or i18n, by using the Variants Block.
Localization in BaseHub is enabled by the Variant Block.
Somewhere in the Editor, press “/variants” and add the block. Name it “Language“.
Add some variants for the languages you’ll support.
Take a look at this short video to see how we do it.
After committing your changes, you will be able to apply variants arguments on the blocks that you enabled it, check it out:
{
# from this point on, the schema will inherit the variant selected
posts(variants: { language: es }) {
items {
_title
excerpt
coverImage {
url
}
body {
json {
content
}
}
}
}
}
Note that variant sets can only live inside Documents. Because of their unique nature, they cannot be replicated by Components and instances behaviors.
Get started with Astro and BaseHub.
Astro is a framework for performant, content-driven websites. With it, you can use almost any UI library you want to (React, Vue, Svelte, etc).
The main difference that the setup Astro has vs Next.js is in the way it exposes environment variables.
While in Next.js, process.env.BASEHUB_TOKEN
is available for our SDK to use, in Vite-powered frameworks (like Astro), you’ll need to explicitly pass the token via params as you’ll see below.
basehub
Our official JavaScript/TypeScript library exposes a CLI generator that, when run, will generate a type-safe GraphQL client. Check out our API Reference for more information.
Install with your preferred package manager.
npm i basehub
BASEHUB_TOKEN
Environment Variable2Get it from your BaseHub Repo’s README.
BASEHUB_TOKEN="<your-token>"
# Remember to also add this ^ env var in your deployment platform
In order to generate the BaseHub SDK, we recommend running basehub dev
in parallel to running the development server, and basehub
right before building the app.
"scripts": {
"dev": "basehub dev & astro dev",
"start": "basehub dev & astro dev",
"build": "basehub && astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
Give it a go to make sure the set up went correctly.
npm run dev
Now, let’s go ahead and query some content!
---
import { basehub } from 'basehub'
const data = await basehub({
token: import.meta.env.BASEHUB_TOKEN
}).query({
__typename: true,
_sys: {
id: true
}
})
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<pre><code>{JSON.stringify(data, null, 2)}</code></pre>
</body>
</html>
While you can query BaseHub content from Astro, there are some DX features that are not supported.
Get started with SvelteKit and BaseHub.
SvelteKit is a framework for rapidly developing robust, performant web applications using Svelte.
The main difference that the setup SvelteKit has vs Next.js is in the way it exposes environment variables.
While in Next.js, process.env.BASEHUB_TOKEN
is available for our SDK to use, in Vite-powered frameworks (like SvelteKit), you’ll need to explicitly pass the token via params as you’ll see below.
basehub
Our official JavaScript/TypeScript library exposes a CLI generator that, when run, will generate a type-safe GraphQL client. Check out our API Reference for more information.
Install with your preferred package manager.
npm i basehub
BASEHUB_TOKEN
Environment Variable2Get it from your BaseHub Repo’s README.
BASEHUB_TOKEN="<your-token>"
# Remember to also add this ^ env var in your deployment platform
In order to generate the BaseHub SDK, we recommend running basehub dev
in parallel to running the development server, and basehub
right before building the app.
"scripts": {
"dev": "basehub dev & vite dev",
"build": "basehub && vite build",
"preview": "vite preview",
... rest scripts
},
Give it a go to make sure the set up went correctly.
npm run dev
Now, let’s go ahead and query some content!
import type { PageServerLoad } from "./$types"
import { basehub } from "basehub"
import { BASEHUB_TOKEN } from "$env/static/private"
export const load: PageServerLoad = async () => {
const now = Date.now()
const data = await basehub({ token: BASEHUB_TOKEN }).query({
__typename: true,
_sys: {
id: true,
},
})
return data
}
While you can query BaseHub content from SvelteKit, there are some DX features that are not supported.
Generates a type-safe client based on your Repo's schema.
basehub generate
This command will get your BaseHub Token from your environment using dotenv-mono, use that to query the BaseHub API, and generate a type-safe SDK out of its schema.
basehub dev
The dev
command is very useful for local development. It basically runs basehub generate --watch --draft [...args]
(notice how watch and draft mode are both being forced).
The main method to consume data from your BaseHub repositories.
import { basehub } from 'basehub'
basehub().query({ })
When your token is setup, basehub.query()
will query the data from the token’s repository.
Inside the query object, you can pass any parameter that the Fetch API allows. That includes the Next.js revalidate parameters.
You can check out its usage in the GraphiQL Explorer linked to your schema.
Our <Pump/>
component uses basehub.query()
behind the scenes to retrieve the data, that’s way its props are so similar. The great advantage <Pump/>
has it’s that it will stream your updates in real time while on draft mode.
Let’s see some common query patterns. Remember, the specific query keys you use will depend on your repository's structure, not on static API definitions.
basehub().query({
homepage: {
title: true,
description: {
json: { content: true },
},
},
})
basehub().query({
posts: {
items: {
_id: true,
_title: true,
_slug: true,
// more fields here...
},
},
})
basehub().query({
posts: {
__args: { first: 1 },
items: {
_id: true,
_title: true,
_slug: true,
// more fields here...
},
},
})
_slug
basehub().query({
posts: {
__args: {
first: 1,
filter: { _sys_slug: "your-post-slug" },
},
items: {
_id: true,
_title: true,
_slug: true,
// more fields here...
},
},
})
basehub().query({
posts: {
__args: {
orderBy: "_sys_createdAt__DESC",
},
items: {
_id: true,
_title: true,
_slug: true,
// more fields here...
},
},
})
import { basehub, fragmentOn } from "basehub"
const buttonFragment = fragmentOn("ButtonComponent", {
label: true,
href: true,
variant: true,
})
basehub().query({
homepage: {
title: true,
description: {
json: { content: true },
},
cta: buttonFragment,
},
})
basehub().query({
dynamicPages: {
items: {
pathname: true,
sections: {
__typename: true, // required
on_HeroSectionComponent: {
title: true,
subtitle: true,
// more fields
},
on_FeatureSectionComponent: {
title: true,
subtitle: true,
// more fields
},
},
},
},
})
The SDK method to make updates to your repository via the API.
import { basehub } from 'basehub'
basehub().mutation({ })
The basehub.mutation()
lets you send GraphQL mutations to the BaseHub API using any JavaScript framework. This is useful for mutating data from your app into your BaseHub Repo.
You can check out its usage in the GraphiQL Explorer linked to your schema.
The mutation
API works a bit different to the query
API due to how GraphQL is designed. basehub().mutation()
has other methods to add data into BaseHub, the most important one being transaction.
The main mutation method, covers most of the modifications that can be done to the BaseHub’s schema with three different transaction types: create, update and delete.
Check out our Mutation API Playground for full examples.
When running the create transaction, you will need to pass two additional parameters: parentId
and data
.
The parentId
is the ID from the block where the creation will be done, could be any block, but that will affect which data structures are valid. In the example above, using that specific parentId
we cannot insert anything apart from instances, because collection children are always instances (or a component that works as template).
The data
field is the new block schema and values, including all its children.
The autoCommit
is an optional field that accepts any string
as the commit message that will be injected into the repository history. If not provided, the mutation updates will stay as work in progress (you will see them listed in your Changes Tab).
Same as `transaction`, but waits until it's resolved.
This method has the same signature as transaction. It’s very useful if you want to wait until the transaction is resolved to know for sure if it succeeded or failed.
Check out our Mutation API Playground for full examples.
A helper to upload assets to our database.
This is useful for example, when we have a PNG on our machine that we want to use in a BaseHub Image block. In order to use it, you should:
getUploadSignedURL
mutation1You will need to retrieve both the signedURL
and the uploadURL
.
signedURL
2The signedURL
is an authorized endpoint that allows you to send any allowed asset through it. You’ll use it to upload the files you want to use in your BaseHub blocks.
uploadURL
3The uploadURL
is the path to the uploaded file. After sending the asset data to the signedURL
, you will be able to see the file in this URL. You will use it in the block value when running a transaction mutation.
On this example we’re uploading a new image file to BaseHub assets pool.
You can explore the full code for this example in Github.
export const uploadImageToBaseHub = async (imageInput: File) => {
const { getUploadSignedURL } = await basehub().mutation({
getUploadSignedURL: {
__args: {
fileName: imageInput.name,
},
signedURL: true,
uploadURL: true,
},
});
const uploadStatus = await fetch(getUploadSignedURL.signedURL, {
method: "PUT",
body: imageInput,
headers: {
"Content-Type": imageInput.type,
},
});
if (uploadStatus.ok) {
return getUploadSignedURL.uploadURL;
}
return null;
};
Gets the current transaction status based on its ID.
You can explore the full code for this example in Github.
export async function getStatus(id: string) {
const response = await basehub().mutation({
transactionStatus: {
__args: {
id,
},
},
});
return response.transactionStatus;
}
A React Server Component that queries BaseHub and can subcribe to real time changes seamlessly.
import { Pump } from 'basehub/react-pump'
Pump is a React Server Component, meaning, it can only be used within frameworks that support RSC (Next.js only for now).
The power of Pump comes when you use Next.js Draft Mode alongside it. Pump lets developers write their queries and rendering logic in a simple and typesafe way, and get “content fast refresh” (live preview) out of the box, without affecting the website’s performance in any way.
If you’re interested in how this works under the hood, or the reason behind its syntax, you can read our blog post about it.
These are the props supported by <Pump />
.
This query will get _sys.id
from the API. Most importantly, when Next.js Draft Mode is enabled, it’ll subscribe to content changes in real time.
import { Pump } from "basehub/react-pump"
import { draftMode } from "next/headers"
const Page = () => {
return (
<Pump
queries={[{ _sys: { id: true } }]}
draft={draftMode().isEnabled}
next={{ revalidate: 30 }}
>
{async ([data]) => {
"use server"
return (
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
)
}}
</Pump>
)
}
export default Page
Our official rich text renderer. Supports passing custom handlers for native html elements and BaseHub components.
import { RichText } from 'basehub/react-rich-text'
A React Component that understands Rich Text Blocks’ data model and helps you render them in your website. If used in frameworks that support server components, it can be used as a RSC and just render the final result in the client without sending all the bundle to the client.
These are the props supported by <RichText />
When you provide the value for the content
property, the RichText component will retrieve your schema types and give you type-safety and auto-complete for the components
property.
import { Code, Heading, Link } from '@radix-ui/themes'
import NextLink from 'next/link'
export const ArticleBody = (props: ArticleFragment) => {
return (
<RichText
content={props.content?.json.content}
blocks={props.content?.json.blocks}
components={{
// native elements not present in this object will use the default handlers
h1: ({ children }) => (
<Heading as="h1" size="3" weight="medium">
{children}
</Heading>
),
h2: ({ children }) => (
<Heading as="h2" size="2" weight="medium">
{children}
</Heading>
),
h3: ({ children }) => (
<Heading as="h3" size="2" weight="medium">
{children}
</Heading>
),
a: ({ children, ...rest }) => (
<Link asChild>
<NextLink {...rest}>{children}</NextLink>
</Link>
),
// Custom component
CodeSnippetComponent: ({ children, isInline }) => {
if (!isInline) return null
return <Code>{children}</Code>
},
}}
/>
)
}
The <RichText />
component doesn’t come with styles. It's job is to render the html notes, but you'll need to add the styles yourself. This is intentional, as websites often vary a lot between typographies, colors, and sizes.
there are a couple ways to style a rich text:
Use regular CSS
Use the tailwindcss-typography plugin (recommended)
Override each of the nodes with your own components
Moreover, all of these methods can be combined as you please.
As an example:
.post-body > *:first-child {
margin-top: 0;
}
.post-body > *:last-child {
margin-bottom: 0;
}
.post-body p {
margin-bottom: 1em;
}
.post-body h1 {
font-size: 2em;
margin: 1.5em 0 0.5em;
}
.post-body h2 {
font-size: 1.8em;
margin: 1.4em 0 0.4em;
}
.post-body h3 {
font-size: 1.4em;
margin: 1.3em 0 0.3em;
}
.post-body ul,
.post-body ol {
margin: 0 0 1em 1.5em;
}
.post-body li {
margin-bottom: 0.5em;
}
.post-body img {
max-width: 100%;
height: auto;
margin: 1em 0;
}
.post-body blockquote {
border-left: 4px solid #ddd;
padding-left: 1em;
margin: 1em 0;
font-style: italic;
color: #666;
}
.post-body pre {
background-color: #f4f4f4;
padding: 1em;
overflow-x: auto;
margin: 1em 0;
}
.post-body code {
background-color: #f4f4f4;
padding: 0.2em 0.4em;
border-radius: 3px;
}
Assuming you’re already using tailwind, using the typography plugin is fairly simple.
pnpm i @tailwindcss/typography --save-dev
Then in tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
}
And finally, you’ll use the prose
class in the wrapping <div>
import { RichText } from "basehub/react-rich-text"
const PostBody = (props) => {
return (
<div className="prose">
<RichText {...props} />
</div>
)
}
If you already have components to render quotes, headings, paragraphs, etc, you can easily use them like this:
import { RichText } from "basehub/react-rich-text"
import {
Heading,
Blockquote,
Paragraph,
} from "@my-design-system"
const PostBody = (props) => {
return (
<div>
<RichText
{...props}
components={{
p: (props) => <Paragraph {...props} />,
h1: (props) => <Heading {...props} level={1} />,
h2: (props) => <Heading {...props} level={2} />,
h3: (props) => <Heading {...props} level={3} />,
blockquote: (props) => <Blockquote {...props} />,
// rest...
}}
/>
</div>
)
}
Example: https://github.com/basehub-ai/docs-template/blob/main/app/_components/article/index.tsx#L182
If you are using Custom Blocks in your Rich Text, you’ll need to add them to your query, and pass them via the blocks
prop. Then, you’ll be able to set up the custom renderers for them (in a type-safe manner, by the way):
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
Easy-to-use component for rendering great code snippets.
import { CodeBlock } from 'basehub/react-code-block'
There are many syntax highlighting libraries in the JavaScript ecosystem. While this is a good thing, it can be exhausting for developers to shop for the right one. Prism, highlight.js, and Shiki are the most popular ones—an, in our opinion, Shiki is the best.
After building multiple websites with syntax highlighting, we’ve found ourselves copy-pasting a bunch of code and needing to re-read documentation over and over again. This is why we’ve built this.
These are the props supported by <CodeBlock />
import { CodeBlock } from 'basehub/react-code-block'
const Post = () => {
return (
<CodeBlock
theme="github-dark"
snippets={[{ code: `const hello = "world"`, lang: 'js' }]}
/>
)
}
import { CodeBlock } from 'basehub/react-code-block'
const Post = () => {
return (
<CodeBlock
theme="github-dark"
snippets={[{ code: `const hello = "world"`, lang: 'js' }]}
/>
)
}
A React hook that instantiates your Search Client.
import { useSearch } from 'basehub/react-search'
BaseHub Search uses TypeSense on the background. You can check out all the search options on their documentation. Keep in mind that they’re all on camelCase in the useSearch
hook. E.g: filter_by
is listed as filterBy
.
Documentation: Step by step
Also you can check out our search implementation for this Documentation template on GitHub
The Search wrapper works as a provider and comes with some optional props that can come in handy.
import { SearchBox } from 'basehub/react-search'
// -> SearchBox.Root
Documentation: Step by step
Also you can check out our search implementation for this Documentation template on Github
Extends the native HTML Input and consumes the search context in order to fetch hits from the indexed data.
import { SearchBox } from 'basehub/react-search'
// -> SearchBox.Input
The <SearchBox.Input />
extends the native HTMLInputProps
.
Documentation: Step by step
Also you can check out our search implementation for this Documentation template on Github
Use cases and APIReference for HitList, HitItem, HitSnippet
import { SearchBox } from 'basehub/react-search'
// -> SearchBox.HitList
import { SearchBox } from 'basehub/react-search'
// -> SearchBox.HitItem
The <SearchBox.HitItem />
extends the native HTMLDivProps
.
Both HitList and HitItem provide keyboard navigation out-of-the-box.
import { SearchBox } from 'basehub/react-search'
// -> SearchBox.HitSnippet
The HitSnippet works as sugar syntax to render specific fields of the hit object with ease.
The events method to send data through BaseHub. Flexible, type-safe and scoped by block.
import { sendEvent } from 'basehub/events'
'use client'
// you'll need to run basehub before importing this type 👇🏼
import type { FeedbackEvents['ingestKey'] } from '~/.basehub/schema'
import { sendEvent } from 'basehub/events'
import { Card, IconButton } from '@radix-ui/themes'
import { ThumbsDown, ThumbsUp } from 'lucide-react'
import * as React from 'react'
export const Feedback = ({
ingestKey,
}: {
ingestKey: FeedbackEvents['ingestKey']
}) => {
const [sentFeedback, setSentFeedback] = React.useState<
'positive' | 'negative' | null
>(null)
const handleFeedback = (type: 'positive' | 'negative') => {
if (sentFeedback === type) return
sendEvent(ingestKey, { positive: type === 'positive' })
setSentFeedback(type)
}
return (
<Card variant="classic" size="3">
<IconButton onClick={() => handleFeedback('negative')}>
<ThumbsDown fill={sentFeedback === 'negative' ? 'var(--accent-12)' : 'none'} />
</IconButton>
<IconButton onClick={() => handleFeedback('positive')}>
<ThumbsUp fill={sentFeedback === 'positive' ? 'var(--accent-12)' : 'none'} />
</IconButton>
</Card>
)
}
A query method to retrieve your events stored in BaseHub.
import { getEvents } from 'basehub/events'
Be wary of exposing the adminKey
in the client. Anyone with this key will be able to read and update existing events from that specific block.
import { getEvents } from "basehub/events"
// Table query
const tableData = await getEvents("analytics:pageviews", {
type: "table",
first: 10,
skip: 0,
})
// Time-series query
const timeSeriesData = await getEvents("analytics:pageviews", {
type: "time-series",
range: "month",
})
import { getEvents } from "basehub/events"
import { IncrementViews } from "./increment-views"
import { unstable_noStore } from "next/cache"
import { draftMode } from "next/headers"
import type { PageViews } from "~/.basehub/schema"
export const ViewsFragment = async ({
adminKey,
ingestKey,
increment,
}: {
adminKey: PageViews["adminKey"]
ingestKey: PageViews["ingestKey"]
increment?: boolean
}) => {
unstable_noStore()
const { isEnabled: isDraftMode } = draftMode()
const { data: views } = await getEvents(adminKey, {
type: "time-series",
range: "all-time",
})
return (
<>
{views || "0"}
{increment && !isDraftMode && (
<IncrementViews ingestKey={ingestKey} />
)}
</>
)
}
Method that allows modifying existing events by their ID.
import { updateEvent } from "basehub/events"
{ success: true; eventId: string } | { success: false; error: string }
Method that removes one or more events by their IDs.
import { deleteEvent } from "basehub/events"
{ success: true } | { success: false; error: string }
Core method to perform a search query.
import { search } from 'basehub/search'
Using React? Check out our React helpers right here.
Get the raw search client.
import { getSearchClientRaw } from 'basehub/search'
Using React? Check out our React helpers right here.
The official BaseHub toolbar to manage draft mode and switch branches in your site previews.
import { Toolbar } from 'basehub/next-toolbar'
The Toolbar takes care of setting and managing the draftMode key without any other configuration or manual fetch to the BaseHub API.
import { Toolbar } from 'basehub/next-toolbar'
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<Toolbar />
<body>
<ThemeProvider>
<Header />
{children}
<Footer />
</ThemeProvider>
</body>
</html>
)
}
Upgrade to version 8.0.0
Learn how to upgrade from version 7.x to 8.0.0.
basehub/events
: a new package used to interact with the Event Block.
basehub
and <Pump />
now automatically infer draftMode
from Next.js.
<Toolbar />
now includes a Branch Switcher!
basehub/analytics
has been deprecated in favour of basehub/events
. They are slightly different things, but Events should be able to cover analytics use cases, and more.
<CodeBlock />: lang
was renamed to language
to better match the props of the pre
handler in <RichText />
<RichText />: The code
handler before received a prop named isInline
, but now, it won’t receive that and it will just be used for inline code. The pre
handler will be used for full code blocks.
Now, if a Reference Block has just one “allowed type”, we won't type it as a GraphQL Union, but rather, just return the end-type directly. this might break queries that did the ... on SomeType
thing.
# before
{
someReference {
... on AuthorComponent {
name
}
}
}
# after
{
someReference {
name
}
}
In our Mutation API, we renamed the following:
transaction
is now transactionAsync
transactionAwaitable
is now transaction
A bit confusing, yes, but we found that transactionAwaitable
(which executed the transaction and responded with the result) was much more useful than the old transaction
, which fired off a job and then it was up to the developer to poll for the transactionStatus
. The name "transactionAwaitable" was a poorly thought out name, and we've taken the opportunity of a breaking version to fix this.
That should be all!
Explore the GraphQL API interactively, thanks to the power of GraphiQL.
By default, this Explorer is connected to our documentation’s repo; but you can enter your repo’s token to explore it’s schema. All credits for this explorer go to the GraphiQL project.
Due to how dynamic the schema is, we believe the best way to know how it’s structure is to use the explorer and check it out. Open the explorer’s documentation, try out some queries, and you can always let us know if you need any help.
You can think of your Repository as a tree of Blocks. Let's explore how this works.
Similar to Notion’s data model, every piece of content in BaseHub is a Block. You can think of your Repository as a tree of Blocks. It all starts with the Root Block—although you won’t see it—, which has nested Blocks within.
We conceptually split Blocks into two categories: Layout, and Primitive Blocks. In the next sections, you’ll read more about them.
Blocks contain schema and content. In contrast to other CMSs, where developers define a schema and then add the content, in BaseHub, this can happen at the same time due to how flexible our data model is.
As you add new Blocks and nest and reorder them, you’ll be altering your Repo’s Schema. That is, the structure that will be then distributed via GraphQL and into your website. Schemas become composable with a special Block called “Component”. Read more about it here.
This is how Blocks get rendered.
In this case, a Text Block.
To add new blocks.
Makes it easy to get around your Repo.
Shows active Blocks.
The root of the Tree.
The Root Block is invisible from the editor explorer, but wraps within every other Block in the repository.
Each Commit in BaseHub targets a single root block. This root block wil target nested blocks, and nested blocks can have more nested blocks, thus creating a tree.
The most common layout block in BaseHub. Think of them as directories in a file system.
✅ Ideal for singleton types, such as a “homepage”
✅ Supports Analytics
✅ Can have nested blocks
✅ Can be converted into a Component
✅ Can be hidden
Although you’re free to structure your Repository however you like, there are some common patterns that can be useful for getting started.
Have a “Components” document in which you’ll store common, reusable components, such as “Button”, “Feature Card“, “Tweet“, or similar.
Have a “Collections” document in which you’ll have different collections, such as “People”, “Testimonials”, “Snippets”, or similar.
Have a “Settings” document in which you’ll store general data, such as site-wide metadata, or constants such as social media links.
The Component block functions as a modular structure within your repository, that can be reused across your schema.
✅ Ideal for reusable types, such as a “CTA” or “Article”
✅ Supports Analytics
✅ Can have nested blocks
✅ Can be converted into a Document
✅ Can be hidden
Each Component outlines a schema, and the content for each instance is then defined within those parameters. The main difference here with other CMSs is that the Component is a source of content at the same time that defines the schema.
To create a Component, you can start from scratch or transform an existing Document into a Component. To do this, simply click the "Make Component" button found in the Document properties panel. This action changes the block type, enabling its reuse as an instance throughout your project.
If you need to transform a Component back into a Document, or if you wish to convert an instance into a standalone Component, you will need to detach the main Component first. By selecting the “Detach Component” button, you’ll convert that block back into a Document. An Instance will be then converted into a Component with that same structure, thus preserving all of the existing instances as they are. No data will be lost during this transition; however, existing instances will now target this new “promoted” instance.
Unlike other structures, Components cannot embed Documents within them. If your design calls for a more layered structure with nested layout blocks, you can achieve this by nesting multiple components or instances. To nest components, simply access the slash command within your main component; it will display the option to insert another component right inside it.
You can define some helpful display information for each Component, so that content editors can understand more about how to use it.
You’ll be able to edit Display Info in the Properties Panel (right hand side) of a Component.
A modular block that reuses the structure from your Components.
✅ Ideal for reusable types, such as a “CTA” or “Article”
✅ Supports Analytics
✅ Can have nested blocks—although it follows the structure of its target component
✅ Can be hidden
Instances are created from components stored in your repository and can be used in all sorts of ways, fitting into many different scenarios. For example, a component and its instances can be listed in a Document block to structure sections of a landing page. Alternatively, you could create a component in a union block with multiple fields, and create various instances from it.
Instances have their own values, so you can update its title and fill every child input. That includes rows in a collection, references and any other input.
You cannot modify the schema in any way, you cannot modify constraints or collection columns, allowed types, children titles, etc.
The OG Image block can only be modified in the main component. That’s the case because every change in the OG Image block is a properties change. But that doesn’t mean that every instance will have the exact same OG image, since you can use variables for text and images, so they will automatically update those based on the current instance they’re set in. Learn more about the OG Image.
A powerful list of blocks that can be fully customizable.
✅ Ideal for repeatable content, such as a list of “Posts“, “Authors“
✅ Can be searched through (using BaseHub Search)
✅ Updates can be tracked by Workflow blocks.
As its name implies, you can make a Collection out of anything in BaseHub. Each new collection starts with an empty "Template" Component, which can be customized or replaced with an existing Component from your schema to serve as its template. When a new row is added to the collection, an empty instance of this template component is automatically appended to the collection’s children list.
Also, you can customize the visibility of collection columns according to the specific needs of different collection types. For instance, if you require a simple image carousel without titles, you have the option to hide the title column, resulting in a cleaner and more streamlined user interface.
If you have a structure in mind in which you have recursion, that is, a block that has nested blocks, that can have more nested blocks (infinitely), you can achieve this via collections.
Let’s take this documentation as an example. As you can see in the sidebar, some articles have nested articles within. This is fully defined by the content editor, as they’re free to nest and nest virtually infinitely. The key here is to have a component that has a child collection that targets the parent component itself (src).
Gives you the option choose between different component structures within a single block.
✅ Ideal for modular sections and programatic pages
✅ Gives you the option choose between different component structures within a single block
✅ Can have nested blocks
The Union type is a powerful block that enables you to create a space for a singular or multiple instances of the selected types. Think of it as an “OR” between the allowed types selected. For example, if you have a Union with allowed types “Code Group” and “Code Snippet”, the union child can be a “Code Group” instance OR a “Code Snippet” instance.
This makes it ideal for a bunch of use cases, including:
Creating a “Sections” union block for your modular, programmatic pages (see example)
Creating a conditional structure, such as “Section with Media“ in which the media part is a union of “either an image or a video”
Lets you create variations of a piece of content. Useful for i18n and A/B testing.
✅ Ideal for i18n and A/B tests
✅ Lets you easily create variations of content
✅ Fully type-safe
Read our guide for Localization here.
The default plain text input in BaseHub. Cannot contain rich text formatting.
✅ Ideal for plain text headings, links or labels
✅ Can be search indexed
✅ Supports AI Chat
✅ Can be used to filter via GraphQL
The most primitive number input in BaseHub. Allows integer, float, negative and positive numbers.
A primitive flag input. True or false.
A primitive date input, with optional time.
A powerful text input that not only supports markdown syntax but also it has the possibility to have many custom components made in BaseHub.
The Rich Text block is the most flexible primitive in BaseHub. Playing with its constraints, you can go from a simple text input with bold, italic and underline formatting to a complex text editor with many custom components present in the repository and integrated in your codebase. It supports Markdown, image copy and pasting, image and video captions, code snippets and more. This tool is particularly effective for those looking to compose articles, blog posts, or any extensive text work that benefits from added visual components and structured formatting.
✅ Ideal for articles, blog posts or long texts
✅ Can be search indexed
✅ Supports AI Chat
Comes with some constraints that can make your image uploads more reliable.
✅ Supports JPG, PNG, SVG, WEBP, AVIF, GIF formats
✅ Supports AI Chat (with image generation)
✅ Auto generated alt text
Comes with some constraints that can make your video uploads more reliable.
Comes with some constraints that can make your audio uploads more reliable.
Comes with some constraints that can make your file uploads more reliable.
Choose from a pre-defined selection of text options.
✅ Ideal for fixed length string list, such as “tags” in a post or “variant” in a button component.
✅ Displays an combobox with options
✅ Options are type safe in the SDK
Choose from a constrained list of component types.
A primitive color input. Opens a palette, and lets you select every color format.
A lightweight OG Image editor used for social cards. Accepts variables and is fully customizable.
✅ Uses @vercel/og under the hood
✅ Renders a lightweight editor
✅ Can use block variables (like the preview button does) to build the design
When you design an OG Image Block within a Component, you’re defining sort of the “template” that will be used by all of the Instance Blocks targeting it. You won’t be able to edit the OG Image within an Instance. That’s why, you’ll probably want to leverage variablesx, such as the ones the preview button has.
It doesn’t have any.
A unique block that enables type-safe data submissions
The Event block is a unique primitive in BaseHub. Playing with its layout and schema, you can go from a simple page view counter to a complex form submissions table.
The events are tracked in real time, so you will see the incoming events no matter the block’s layout.
✅ Ideal for tracking analytics events or form submissions
✅ Can have a type-safe schema, used to render forms or send events through the SDK.
✅ Can be tracked by Workflow blocks.
A unique block to automate actions, webhooks and notifications.
The Workflow block acts as a listener for different triggers in your repo. You can track new commits, incoming events, or collection row modifications. Each workflow can trigger multiple actions, ranging from webhooks to user notifications.
✅ Ideal for automating notifications, setting up a newsletter or tracking repository updates.
✅ Can track collections, event blocks and new commits.
✅ Can setup webhooks with useful payload data.
✅ Can notify any user in the repo.
It doesn’t have any.
Know more about how templates work in the platform.
Templates are a great way to optimize repeatable workflows. Building a landing page? A blog? A documentation site? Use one of our official templates, or just create one for you and the community to enjoy. They’ll surely give you a head start in your next project.
For a template to work, it needs a GitHub Repo and a BaseHub Repo that are connected and understand each other. Users of the template will:
Fork the GitHub Repo, so they’ll own the code.
Fork the BaseHub Repo, so they’ll own the content.
Deploy the code to Vercel.
Once deployed, the user should be able to start modifying stuff (content and/or code) as they please.
As seen in the video above, once you click the Vercel Deploy Button, you’ll be redirected to Vercel. There, you’ll create a GitHub Repository based on the template’s GitHub Repository. Then, you’ll add the BaseHub Integration. In the BaseHub Integration, you’ll create a new BaseHub Repository based on the template’s BaseHub Repository. After doing so, you’ll go back to Vercel to finish up the deploy flow.
At the end of it, you’ll have the code, the content, and the Vercel Project.
The perfect way to start your next marketing website.
BaseHub Repo: https://basehub.com/basehub/marketing-website
GitHub Repo: https://github.com/basehub-ai/marketing-website-template
This template is great for:
SaaS companies
AI startups
Indie hackers
… and more!
Once you fork and deploy the template, you’ll own the code and the content. You’ll be able to edit all of its copy, change the logo and accent color, and add new sections and programmatic pages.
The template that powers this documentation website.
BaseHub Repo: https://basehub.com/basehub/docs
GitHub Repo: https://github.com/basehub-ai/docs-template
This template is great for:
Developer Docs
Company Handbooks
User Manuals
… and more!
Once you fork and deploy the template, you’ll own the code and the content. You’ll be able to edit all of its copy, change the logo and accent color, and add new sections and articles.
The template that powers our own Help Center. Full-text search included.
BaseHub Repo: https://basehub.com/basehub/help-center
GitHub Repo: https://github.com/basehub-ai/help-center-template
This template is great for:
Help Centers
FAQs and Knowledge Bases
… and more!
Once you fork and deploy the template, you’ll own the code and the content. You’ll be able to edit all of its copy, change the logo and accent color, and add new sections and articles.