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

  • categoryIds must 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:

  • cmsPostscpPostList
  • categoryIdcategoryIds (array format)
  • Results are nested under posts field
  • Added totalCount for 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., categoryIdcategoryIds)
  • 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