CMS + Next.js: CMS Migration
This section provides a comprehensive guide for migrating from an existing web-builder setup to the Erxes CMS when working with Next.js applications.
Overview
Migrating from a web-builder setup to Erxes CMS involves updating your configuration and queries to work with the Erxes GraphQL API. This guide will help you transition your existing Next.js application to use Erxes CMS for content management.
Prerequisites
Before starting the migration process, ensure you have:
- An active Erxes SaaS instance with CMS plugin installed
- A Client Portal created in Erxes
- A valid Client Portal Token (not a system token)
- Your current Next.js project with Apollo Client
- Access to your existing web-builder configuration
1. Environment Configuration (next-config.js)
Update your environment variables to point to the erxes CMS domain and use the Client Portal token.
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
ERXES_API_URL: "https://[your-domain].next.erxes.io",
ERXES_APP_TOKEN: "YOUR_CLIENT_PORTAL_TOKEN",
},
};
module.exports = nextConfig;
Notes
- ERXES_API_URL must be your erxes CMS domain
- ERXES_APP_TOKEN must be a Client Portal token, not a system token
2. Apollo Client Configuration
When working with CMS APIs, erxes requires the token to be sent using the x-app-token header.
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const httpLink = createHttpLink({
uri: `${process.env.ERXES_API_URL}/gateway/graphql`,
});
const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
"x-app-token": process.env.ERXES_APP_TOKEN,
},
}));
export const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
Key Change
- erxes-app-token ➜ x-app-token
3. CMS GraphQL Queries
Post List Query
Use cpPostList to fetch CMS posts.
import { gql } from "@apollo/client";
export const cmsPostList = gql`
query PostList(
$type: String
$featured: Boolean
$categoryIds: [String]
$searchValue: String
$status: PostStatus
$tagIds: [String]
$sortField: String
$sortDirection: String
) {
cpPostList(
featured: $featured
type: $type
categoryIds: $categoryIds
searchValue: $searchValue
status: $status
tagIds: $tagIds
sortField: $sortField
sortDirection: $sortDirection
) {
totalCount
posts {
_id
title
content
excerpt
featured
status
createdAt
updatedAt
thumbnail {
url
}
categories {
_id
name
}
images {
url
type
name
}
}
}
}
`;
Single Post Query
Use cpPostDetail to fetch a specific post by slug or ID.
export const cmsPostDetail = gql`
query PostDetail($slug: String, $id: String) {
cpPostDetail(slug: $slug, _id: $id) {
_id
title
content
excerpt
featured
status
createdAt
updatedAt
thumbnail {
url
}
categories {
_id
name
}
images {
url
type
name
}
author {
... on User {
_id
email
}
... on ClientPortalUser {
fullName
firstName
lastName
}
}
}
}
`;
4. Fetching Data in Components
Example: Fetching Posts with SSR
import { useQuery } from "@apollo/client";
import { cmsPostList } from "@/graphql/queries";
export default function BlogPage() {
const { data, loading, error } = useQuery(cmsPostList, {
variables: {
categoryIds: ["c-exNoBpJp4WUd1fgUB9m"],
status: "published",
},
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error loading posts</div>;
const posts = data?.cpPostList?.posts || [];
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post._id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<span>Published: {new Date(post.createdAt).toLocaleDateString()}</span>
</article>
))}
</div>
);
}
Example: Fetching Single Post with CSR
"use client";
import { useQuery } from "@apollo/client";
import { cmsPostDetail } from "@/graphql/queries";
export default function PostPage({ params }) {
const { data, loading, error } = useQuery(cmsPostDetail, {
variables: {
slug: params.slug,
},
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error loading post</div>;
const post = data?.cpPostDetail;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<div>By: {post.author.fullName || post.author.email}</div>
</article>
);
}
Important Notes
categoryIdsmust be an array of strings- Always handle loading and error states
- Use SSR for pages that need SEO optimization
- Use CSR for interactive components
5. Common Issues
CORS Errors
- The domain is not whitelisted in Client Portal settings
- Incorrect API URL
Unauthorized Errors
- Invalid or missing Client Portal token
- Wrong header name (x-app-token required)
5. Migrating Existing Queries
From cmsPosts to cpPostList
If you're migrating from the existing cmsPosts query, update your queries:
Before:
import { gql } from "@apollo/client";
export const cmsPosts = gql`
query CmsPosts($clientPortalId: String, $featured: Boolean, $type: String, $categoryId: String, $searchValue: String, $status: PostStatus, $page: Int, $perPage: Int, $tagIds: [String], $sortField: String, $sortDirection: String, $language: String) {
cmsPosts(clientPortalId: $clientPortalId, featured: $featured, type: $type, categoryId: $categoryId, searchValue: $searchValue, status: $status, page: $page, perPage: $perPage, tagIds: $tagIds, sortField: $sortField, sortDirection: $sortDirection, language: $language) {
_id
title
content
// ... other fields
}
}
`;
After:
import { gql } from "@apollo/client";
export const cmsPostList = gql`
query PostList(
$type: String
$featured: Boolean
$categoryIds: [String]
$searchValue: String
$status: PostStatus
$tagIds: [String]
$sortField: String
$sortDirection: String
) {
cpPostList(
featured: $featured
type: $type
categoryIds: $categoryIds
searchValue: $searchValue
status: $status
tagIds: $tagIds
sortField: $sortField
sortDirection: $sortDirection
) {
totalCount
posts {
_id
title
content
// ... other fields
}
}
}
`;
Key Changes:
cmsPosts→cpPostListcategoryId→categoryIds(array format)- Results are nested under
postsfield - Added
totalCountfor pagination
6. Common Issues
CORS Errors
- The domain is not whitelisted in Client Portal settings
- Incorrect API URL
Unauthorized Errors
- Invalid or missing Client Portal token
- Wrong header name (x-app-token required)
Query Errors
- Field name changes (e.g.,
categoryId→categoryIds) - Missing required fields in variables
- Incorrect data types for GraphQL variables
7. Validation Checklist
- [ ] Correct erxes CMS domain in
ERXES_API_URL - [ ] Client Portal token configured in
ERXES_APP_TOKEN - [ ] x-app-token header used in Apollo Client
- [ ] GraphQL variables have correct types
- [ ] Query field names updated to new format
- [ ] Domain whitelisted in Client Portal settings
- [ ] Test queries with Apollo DevTools
- [ ] Verify SSR and CSR components work correctly
Next Steps
- Implement Single Post Query (cpPostDetail)
- Add Pagination using pageInfo
- Enable SSG / ISR for CMS pages
- Add SEO metadata from CMS fields
- Migrate existing pages to use new query structure
- Update error handling for new API responses
This guide is fully compatible with MDX-based documentation systems such as next-mdx-remote, contentlayer, or custom doc portals.
Edit this page