# SSR Fetching

Fetch CMS data on the server in Next.js App Router pages and handle caching safely


Render CMS content on the server so the initial HTML contains the page's content. This example uses the server helper from [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) and the post operations in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries).

## Fetch published posts

Create a reusable server module. The publication filter is fixed in the document, so callers cannot replace it with `draft`.

```ts
// lib/erxes/posts.ts
import "server-only";
import { cache } from "react";
import { erxesQuery } from "./server";

export type PostSummary = {
  _id: string;
  title: string | null;
  slug: string | null;
  excerpt: string | null;
  status: string | null;
};

type PostListData = {
  cpPostList: {
    posts: Array<PostSummary | null> | null;
  } | null;
};

const POST_LIST_QUERY = `
  query WebsitePublishedPosts($searchValue: String) {
    cpPostList(status: published, limit: 12, searchValue: $searchValue) {
      posts {
        _id
        title
        slug
        excerpt
        status
      }
    }
  }
`;

export async function getPublishedPosts(searchValue = "") {
  const data = await erxesQuery<PostListData>(POST_LIST_QUERY, { searchValue });
  return (data.cpPostList?.posts ?? []).filter(
    (post): post is PostSummary =>
      post !== null && post.status === "published" && Boolean(post.slug),
  );
}

const POST_QUERY = `
  query WebsitePost($slug: String!) {
    cpPost(slug: $slug) {
      _id
      title
      slug
      excerpt
      status
    }
  }
`;

export const getPublishedPost = cache(async (slug: string) => {
  const data = await erxesQuery<{ cpPost: PostSummary | null }>(
    POST_QUERY,
    { slug },
  );
  const post = data.cpPost;
  return post?.status === "published" ? post : null;
});
```

The [post query builder](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/query-builders.ts) only adds a status filter when one is supplied. The [detail resolver](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts) does not itself restrict the result to published posts. Check the status before returning a detail record or using its fields in metadata.

## Render the index

```tsx
// app/blog/page.tsx
import { getPublishedPosts } from "@/lib/erxes/posts";

export default async function BlogPage() {
  const posts = await getPublishedPosts();

  return (
    <main>
      <h1>Blog</h1>
      {posts.length === 0 ? <p>No published posts yet.</p> : (
        <ul>
          {posts.map(post => (
            <li key={post._id}>
              <a href={`/blog/${encodeURIComponent(post.slug!)}`}>
                {post.title || "Untitled post"}
              </a>
              {post.excerpt ? <p>{post.excerpt}</p> : null}
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}
```

This example displays the first 12 matching posts. For a complete archive, add cursor pagination using the `pageInfo` fields documented in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries).

## Render a detail route

This route-prop type follows the asynchronous `params` API in [Next.js 15 and later](https://nextjs.org/docs/app/api-reference/file-conventions/page#params-optional), including the product's Next.js 16 portal and help-center apps. If your separate website uses Next.js 14, use `params: { slug: string }` instead; the `await params` line can remain.

```tsx
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getPublishedPost } from "@/lib/erxes/posts";

type Props = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPublishedPost(slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title || "Untitled post"}</h1>
      {post.excerpt ? <p>{post.excerpt}</p> : null}
    </article>
  );
}
```

This minimal example displays plain text. To render the full `content` field, determine its stored format and use an appropriate renderer. If it contains HTML, sanitize it with a maintained server-side HTML sanitizer before inserting it as markup. Do not insert untrusted content directly with `dangerouslySetInnerHTML`.

For SEO metadata, call the same `getPublishedPost` helper from `generateMetadata`; return missing-page metadata when it returns `null`.

## Caching and view counts

The Setup helper uses `cache: "no-store"`. Adding `export const revalidate = 300` to a page does not turn those uncached requests into five-minute cached fetches. Choose caching at the data layer when the content and authorization model allow it. See [Next.js 14 data fetching and caching](https://nextjs.org/docs/14/app/building-your-application/data-fetching/fetching-caching-and-revalidating).

React's `cache` wrapper deduplicates the detail helper within a server rendering request; it is not persistent storage across requests. `cpPost` increments the post view count, so repeated reads can increase that count while caching can reduce counted reads. It is not a reliable count of unique human visitors.

For draft previews or user-specific content, keep caching and authorization separate from the public published-content path.

## Loading and errors

Add an `app/blog/loading.tsx` loading UI and an `app/blog/error.tsx` error boundary with a useful retry action. A missing or unpublished record should use the not-found path; a failed upstream request should remain an error.

Verify a published post, a draft, an unknown slug, an empty list, and an unavailable gateway. Continue with [Client-side Rendering](https://erxes.io/docs/content-plug-cms-next-developers-csr) for interactive search.

## Troubleshooting

- **`data.cpPostList` is null or an HTTP error**: the `x-app-token` is missing or invalid. Confirm the token and gateway URL in the server environment.
- **Empty `posts` array**: the portal token matched, but the portal has no published posts; check the records' `clientPortalId` association and `status`.
- **Unknown argument errors**: compare your document with the signatures in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries); most lists use `limit`/`cursor`, not `page`/`perPage`.

