Manual Setup
Learn how to install and configure the @query-api/next package for your Next.js project.
This guide is for developers who want to integrate the Query API into an existing Next.js 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 Next.js 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. Next.js App
You'll need a Next.js application. If you're starting from scratch, you can create one inside your Craft project's root folder.
npx create-next-app@latest frontend --app --ts --src-dir --empty
You can now open the frontend
directory in your code editor to begin the setup.
Installation and Folder Structure
First, install the @query-api/next
SDK in your Next.js project.
npm install @query-api/next
Next, we will create the following folder and file structure inside the src
directory. This structure helps organize your code by separating concerns.
├── src
│ ├── app
│ │ ├── [[...slug]]
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── components
│ │ ├── content
│ │ │ ├── BlockHeadline.tsx
│ │ │ └── ViewHome.tsx
│ ├── libs
│ │ └── query-api.ts
│ ├── types
│ │ └── base.ts
│ └── middleware.ts
├── .env
Environment Variables
Create a .env
file in the root of your Next.js project to store your Craft CMS connection details.
# Allows Node.js to connect to local development URLs (e.g., DDEV).
# Remove this in production.
NODE_TLS_REJECT_UNAUTHORIZED=0
# The base URL of your Craft CMS backend.
NEXT_PUBLIC_CRAFT_BASE_URL=https://query-api-starter.ddev.site
# The bearer token you generated in the Query API plugin settings.
NEXT_PUBLIC_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" 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/next'
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>
)
}
Middleware Setup
Middleware is used to share request context globally, which is essential for features like live preview to work correctly without needing to pass request data down through every component.
import { createQueryApiMiddleware } from '@query-api/next/server'
// This function initializes the middleware with default settings.
export default createQueryApiMiddleware()
export const config = {
// The matcher ensures the middleware runs on all paths except for
// API routes, TRPC routes, Next.js internal paths, and static files.
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
}
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 blocks.
import { craftInit, CraftNotImplemented } from '@query-api/next/server'
// 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: process.env.NEXT_PUBLIC_CRAFT_BASE_URL ?? '',
authToken: process.env.NEXT_PUBLIC_CRAFT_AUTH_TOKEN ?? '',
// Define a site map for multi-site setups.
siteMap: [
{ handle: 'en', path: '/', origin: 'http://localhost:3000', id: 1 },
{ handle: 'de', path: '/de', origin: 'http://localhost:3000', id: 2 },
{ handle: 'es', path: '/es', origin: 'http://localhost:3000', 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,
}
}
})
Root Layout
The CraftClientProvider
must wrap your root layout to make the configuration available to all components, including client-side components.
import Link from "next/link";
import { CraftClientProvider } from '@query-api/next/server'
import { craftConfig } from '@/libs/query-api';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
// The provider makes the `craftConfig` available throughout the component tree.
<CraftClientProvider config={craftConfig}>
<html lang="en">
<body>
<nav>
<ul>
<li><Link href="/">Home</Link></li>
<li><Link href="/de">Home DE</Link></li>
<li><Link href="/es">Home ES</Link></li>
</ul>
</nav>
{children}
</body>
</html>
</CraftClientProvider>
);
}
Catch-All Route
This dynamic route is the core of the page rendering logic. It captures every incoming URL, fetches the corresponding entry from Craft CMS, and renders it using the CraftPage
component.
import { getCraftUri, getCraftCurrentSite, getCraftEntry, CraftPage } from '@query-api/next/server'
import { CraftPageBase } from '@/types/base'
export default async function CraftCatchAllPage() {
// getCraftUri() determines the page URI based on your currentSite.
const uri = await getCraftUri()
// getCraftCurrentSite() identifies the current site based on the URL (e.g., /de).
const { id } = await getCraftCurrentSite()
// getCraftEntry() starts building a query for a single entry.
// The query is configured with the URI and site ID, and then executed with .one().
const { data, error } = await getCraftEntry<CraftPageBase>()
.uri(uri)
.siteId(id)
.one()
if (error || !data) {
throw new Error(error?.message || 'No data returned from Craft CMS.')
}
// The CraftPage component automatically selects the correct view
// (e.g., ViewHome) from your contentMapping based on the fetched data.
return <CraftPage content={data} />
}
With this setup, navigating to any page on your Next.js 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.