Manual Setup
Learn how to install and configure the @query-api/next package for your React project.
This guide is for developers who want to integrate the Query API into an existing React project or for those who want to understand the setup process step-by-step.
If you prefer to dive straight into code, you can check out the React demo project on GitHub.
Prerequisites
Before you begin, please ensure you have the following set up:
1. Craft CMS with Query API
You need a running Craft CMS instance with the Query API plugin installed and configured.
Additionally, make sure you have at least one section created in Craft CMS. For this guide, we'll assume you have a section with the handle home
and an entry type also named home
.
You can also use the CLI to set up a Craft-only project:
npx create-query-api@latest query-api-starter --template craft-only
You can log into the control panel with these credentials:
- Username:
admin
- Password:
admin123
2. React App
You'll need a React application. If you're starting from scratch, you can create one inside your Craft project's root folder.
npm create vite@latest frontend -- --template react-ts
If you set up your Craft CMS project with the starter cli craft-only
template, ensure that the
dev server url (https://localhost:5173) matches the WEBSITE_URL
environment variable in the .env
of your Craft CMS project.
You can now open the frontend
directory in your code editor to begin the setup.
Installations and Folder Structure
First, install the @query-api/react
SDK and react-router
in your React project.
npm install @query-api/react react-router
Next, create the following folder and file structure inside your src
directory. This structure helps organize your code by separating concerns.
├── src
│ ├── components
│ │ ├── content
│ │ │ ├── BlockHeadline.tsx
│ │ │ ├── ViewHome.tsx
│ ├── libs
│ │ └── query-api.ts
│ ├── types
│ │ └── base.ts
│ ├── App.tsx
│ ├── CraftRouter.tsx
│ ├── main.tsx
│ └── vite-env.d.ts
├── .env
Environment Variables
Create a .env
file in the root of your Next.js project to store your Craft CMS connection details.
# The base URL of your Craft CMS backend.
VITE_CRAFT_BASE_URL=https://query-api-starter.ddev.site
# The bearer token you generated in the Query API plugin settings.
VITE_CRAFT_AUTH_TOKEN="Bearer sqKTlMFsky_OeJVeDfnps75b2Gny4NBG" # Default of create-query-api starter template
You can find/create the bearer token under /admin/query-api/tokens
in the control panel.
Generate Types
The Query API plugin can generate TypeScript types based on your Craft CMS sections and entry types. This provides full type safety for your frontend data.
First, add a script to your composer.json
in your Craft CMS project root.
"scripts": {
"generate-types": "php craft query-api/typescript/generate-types --output=@root/frontend/src/types/base.ts"
}
Now, run the command to generate the base.ts
file. If you are using DDEV, you can run:
ddev composer run generate-types
This will create a base.ts
file in frontend/src/types
with all the necessary type definitions.
Content Driven Components
These are the React components that will render your Craft CMS content. We recommend placing them in a dedicated components/content
directory to distinguish them from general-purpose UI components.
Here is an example of a component for a headline
entry type that is used in a matrix block.
import type { CraftEntryTypeHeadline } from '../../types/base'
// This component renders a headline from a Matrix block.
// The props are fully typed based on the generated `base.ts` file.
export default function Headline(props: CraftEntryTypeHeadline) {
// The `selectHeadlineTag` field is a dropdown in Craft,
// allowing content editors to choose the HTML tag (e.g., h1, h2).
const Tag = props.selectHeadlineTag.value
return <Tag>{props.title}</Tag>
}
Next, create the main view component for your home
section.
import { CraftArea } from '@query-api/react'
import type { CraftPageHome } from '../../types/base'
// This component renders the "Home" page.
export default function Home(props: CraftPageHome) {
return (
<div>
{/* `translatablePlainText` is a field from our Craft entry. */}
<h1>{props.translatablePlainText}</h1>
{/* The CraftArea component dynamically renders Matrix blocks. */}
{/* It takes the `contentBuilder` field (a Matrix field) and maps each block */}
{/* to the corresponding component defined in `query-api.ts`. */}
<CraftArea content={props.contentBuilder} />
</div>
)
}
Query API Configuration
This file initializes the Query API client and defines the contentMapping
, which tells the library which React components to use for different Craft CMS pages and entry types.
import { craftInit, CraftNotImplemented } from '@query-api/react'
// Import the components that will render Craft CMS content.
import ViewHome from '../components/content/ViewHome'
import BlockHeadline from '../components/content/BlockHeadline'
export const craftConfig = craftInit({
baseUrl: import.meta.env.VITE_CRAFT_BASE_URL,
authToken: import.meta.env.VITE_CRAFT_AUTH_TOKEN,
// Define a site map for multi-site setups.
siteMap: [
{ handle: 'en', path: '/', origin: 'http://localhost:5173', id: 1 },
{ handle: 'de', path: '/de', origin: 'http://localhost:5173', id: 2 },
{ handle: 'es', path: '/es', origin: 'http://localhost:5173', id: 3 },
],
// The contentMapping connects Craft handles to your React components.
contentMapping: {
// `pages` maps a section handle (e.g., "home") to a view component.
pages: {
home: ViewHome,
},
// `components` maps a block type handle (e.g., "headline") to a component.
// This is typically used for Matrix blocks.
components: {
headline: BlockHeadline,
// Use CraftNotImplemented as a placeholder for components you haven't created yet.
text: CraftNotImplemented,
imageAndText: CraftNotImplemented,
image: CraftNotImplemented,
}
}
})
Router Setup
We need to use the useLocation()
hook in the App
later on, that's why we need to wrap the <App />
with the BrowserRouter
Provider.
import { StrictMode } from 'react'
import { BrowserRouter } from 'react-router'
import * as ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
)
Catch-All Component
This component will be the router for all Craft CMS pages. We basically query the data based on the current url and render the matching React Component.
import {
CraftPage,
useCraftCurrentSite,
useCraftEntry,
useCraftUri,
} from '@query-api/react'
import type { CraftPageBase } from './types/base'
export default function CraftRouter() {
const uri = useCraftUri()
const { id: siteId } = useCraftCurrentSite()
const { data, loading, error } = useCraftEntry<CraftPageBase>()
.uri(uri)
.siteId(siteId)
.one()
if (error) {
console.error(error)
}
return <div>{!loading && data && <CraftPage content={data} />}</div>
}
App Setup
The CraftProvider
must wrap your root layout to make the configuration available to all components.
import { Route, Routes, Link, useLocation } from 'react-router'
import CraftRouter from './CraftRouter'
import { CraftProvider, getCraftLocation } from '@query-api/react'
import { craftConfig } from './libs/query-api'
export function App() {
const location = getCraftLocation(useLocation())
return (
<CraftProvider config={craftConfig} location={location}>
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/de">Home DE</Link></li>
<li><Link to="/es">Home ES</Link></li>
</ul>
</nav>
<Routes>
<Route path="*" element={<CraftRouter />} />
</Routes>
</div>
</CraftProvider>
)
}
export default App
With this setup, navigating to any page on your React site will trigger a fetch to your Craft CMS backend, and the correct content will be rendered automatically. It's as simple as that! 🚀
Anything missing?
If you have questions, run into issues, or have ideas for improvements, your feedback is very welcome! Please don't hesitate to open an issue on GitHub. Whether it's a bug report, a feature request, or a general suggestion, your input helps make this project better for everyone.