CMS + Next.js: Migration

Update an older website integration to the current erxes API. This is an application-integration migration, not a database migration or a guarantee that every older deployment exposes the same schema.

1. Inventory the current integration

Record the deployed erxes revision, website dependency versions, gateway URL, credential headers, GraphQL operations, and CMS/portal associations. Identify which requests run in the browser and which already run on the server.

Distinguish public website reads from authenticated administration, previews, portal-user actions, and mutations. They may require different credentials and authorization.

2. Move credentials into server modules

Follow Setup. Remove credential values from next.config.env, NEXT_PUBLIC_* variables, static configuration files, and client components. Rebuild the website to remove those values from newly generated browser assets.

The public examples use a Client Portal token through x-app-token. Existing administrative integrations may continue to require an Apps token through erxes-app-token; keep those behind their own authorized server interface.

If an Apps credential was published in browser assets, treat it as exposed and coordinate revocation and replacement through the application's administration. The portal JWT path verifies the signature and portal existence but does not compare the submitted JWT with the portal's stored token (ClientPortal.token). Changing ClientPortal.token alone is therefore not proof that older JWTs are invalid.

3. Update operation names and shapes

Compare each operation with Queries and the schema for your installed version.

Common corrections:

  • Use categoryIds: [String] for post filters, rather than categoryId.
  • Most list operations use limit and cursor. The dedicated cpPostListWithPagination operation accepts page and perPage.
  • A post list envelope contains posts; a page list contains pages; a category list contains list; a tag list contains tags.
  • The post detail field is cpPost; there is no cpPostDetail.
  • The page detail field is cpCmsPageDetail.
  • A menu item's field is contentTypeId, with a lowercase final d.
  • The Author union contains User; do not add an unsupported ClientPortalUser fragment.

The schema directory defines these operations and fields. A nullable schema argument may still be required by a resolver, so inspect runtime behavior as well.

4. Enforce publication policy

Switching an operation from cms* to cp* does not automatically hide draft content. Post lists apply the requested status, while post detail reads do not add a published-only filter.

Use a fixed status: published filter for public post lists. Verify the returned status before exposing a detail record or using its fields in metadata. Check pages, menus, categories, and other content families individually; do not infer their policy from the post API.

The SSR guide demonstrates published-only list and detail handling. The CSR guide exposes a fixed website search endpoint with bounded inputs.

5. Rendering and caching

Keep server credential modules outside the browser dependency graph. A component's imports become part of the client graph when reached through a client boundary, even if each imported file does not declare "use client".

The Setup helper uses cache: "no-store". Decide on a deliberate caching policy before adding revalidation settings; a page-level revalidation declaration does not override an explicitly uncached fetch.

The cpPost detail resolver increments a view counter. Repeated metadata and page queries can increase it, and cached results can reduce counted reads. Verify analytics expectations separately from content rendering.

Render stored content according to its format. Sanitize untrusted HTML before using an HTML insertion API, and preserve the application's image and link handling.

6. Test before switching traffic

Test these cases against a staging instance with known records:

  • Published and draft posts, including direct detail URLs and metadata.
  • Unknown slugs and an empty portal.
  • Cursor boundaries, filters, and expected response envelopes.
  • Missing/invalid credentials and unavailable upstream services.
  • Browser requests and built assets without erxes credentials.
  • Rejected unsupported parameters and operations.
  • Authenticated previews or mutations through their separate authorized flows.
  • Image URLs and rich content rendering.

If introspection is enabled in the development instance, compare the live schema with the query documents. Otherwise use the installed source schema or an administrator-provided export. A production deployment may intentionally disable introspection.

Keep the previous website build and configuration available during rollout. Restoring a website build does not reverse product database migrations; handle a product upgrade according to its own migration procedure.

Troubleshooting

  • 401/403 or GraphQL auth errors after migration: the wrong credential family is being sent; the website needs the Client Portal token in x-app-token, not an Apps token, and not a client-auth-token.
  • Valid token, empty lists: the portal matched but has no matching records; re-check clientPortalId on posts, pages, and menus after a portal change.
  • Unknown argument errors: page/perPage only exist on cpPostListWithPagination; other lists take limit/cursor. Check the Queries reference.
  • Drafts visible on public routes: a cp* prefix does not filter publication status; enforce status: published on lists and check status on detail reads.
  • Stale content after deployment: the Setup helper uses cache: "no-store"; if you added caching, re-check revalidate settings and any CDN layer.
Was this helpful?