Skip to main content
Our docs redesign is live!

Part 2: build the frontend with AI

5 min read

Part 2: build the frontend with AI

Now that the content side is ready, you can start building the application.

Step 6: create the Next.js project

For this guide, we will use Next.js with the App Router and TypeScript.

npx create-next-app@latest my-contentstack-site
cd my-contentstack-site
npm install @contentstack/delivery-sdk @contentstack/utils

Then create a .env.local file:

CONTENTSTACK_API_KEY=your_stack_api_key
CONTENTSTACK_DELIVERY_TOKEN=your_delivery_token
CONTENTSTACK_ENVIRONMENT=development
CONTENTSTACK_WEBHOOK_SECRET=your_webhook_secret

Step 7: prompt the AI with enough structure

At this point, you are ready to start using your assistant for implementation tasks. The important thing is not just to ask for a file. It is to provide enough constraints that the assistant can make good decisions.

Here is a strong example prompt:

I am building a content-driven website with Next.js App Router and Contentstack.
Use TypeScript and @contentstack/delivery-sdk.
Initialize the stack using apiKey, deliveryToken, and environment from process.env.
I have a content type with UID landing_page and these fields:
- page_title
- slug
- main_content
- hero_image
- service_price
- launch_date

Create a reusable helper in lib/contentstack.ts and then create a page at app/services/[slug]/page.tsx.
Fetch a single landing_page entry where slug matches the route parameter.
Return a typed Server Component and render the title, hero image, rich text body, price, and date.

Notice what this prompt does well:

  • it sets the goal

  • it defines the tech stack

  • it names the exact content type

  • it names the fields

  • it describes the desired output

That is the difference between useful AI output and generic filler.

Step 8: create a reusable SDK layer

You should not scatter SDK initialization across random pages. Centralize it in one file and reuse helpers everywhere.

Here is a clean baseline:

// lib/contentstack.ts
import contentstack, { BaseEntry, QueryOperation } from "@contentstack/delivery-sdk";

function getStack() {
  const {
    CONTENTSTACK_API_KEY,
    CONTENTSTACK_DELIVERY_TOKEN,
    CONTENTSTACK_ENVIRONMENT,
  } = process.env;

  if (!CONTENTSTACK_API_KEY || !CONTENTSTACK_DELIVERY_TOKEN || !CONTENTSTACK_ENVIRONMENT) {
    return null;
  }

  return contentstack.stack({
    apiKey: CONTENTSTACK_API_KEY,
    deliveryToken: CONTENTSTACK_DELIVERY_TOKEN,
    environment: CONTENTSTACK_ENVIRONMENT,
  });
}

export async function getEntries<T extends BaseEntry>(contentTypeUid: string): Promise<T[]> {
  const stack = getStack();
  if (!stack) return [];

  const { entries } = await stack
    .contentType(contentTypeUid)
    .entry()
    .query()
    .find<T>();

  return entries ?? [];
}

export async function getEntryBySlug<T extends BaseEntry>(
  contentTypeUid: string,
  slug: string
): Promise<T | null> {
  const stack = getStack();
  if (!stack) return null;

  const { entries } = await stack
    .contentType(contentTypeUid)
    .entry()
    .query()
    .where("slug", QueryOperation.EQUALS, slug)
    .find<T>();

  return entries?.[0] ?? null;
}

There are two things worth calling out here.

First, this uses a lazy getStack() function instead of initializing the SDK at module load time. That matters later when you deploy to Contentstack Launch, because missing environment variables during build will otherwise cause brittle failures.

Second, these helpers give your AI assistant a stable implementation surface. Once this file exists, you can reference it in prompts and ask the assistant to build pages on top of it rather than regenerating connection logic every time.

Step 9: build pages one by one

Now the workflow becomes more natural. You can ask the AI to generate each page against your existing content layer.

For example, a services list page can fetch all landing_page entries and render them as cards. A blog detail page can fetch a blog_post by slug. A homepage can fetch a single homepage entry and render a hero.

Here is a simplified example for a homepage server component:

// app/page.tsx
import { getEntries } from "@/lib/contentstack";

interface HomepageEntry {
  hero_title?: string;
  hero_subtitle?: string;
  cta_text?: string;
  cta_link?: string;
  hero_image?: { url?: string };
}

export default async function HomePage() {
  const entries = await getEntries<HomepageEntry>("homepage");
  const homepage = entries[0];

  if (!homepage) {
    return <main className="p-10">No homepage content found.</main>;
  }

  return (
    <main className="relative min-h-[70vh] overflow-hidden">
      {homepage.hero_image?.url ? (
        <img
          src={`${homepage.hero_image.url}?width=1600`}
          alt={homepage.hero_title ?? "Hero image"}
          className="absolute inset-0 h-full w-full object-cover"
        />
      ) : null}
      <div className="relative z-10 mx-auto flex min-h-[70vh] max-w-5xl flex-col items-center justify-center px-6 text-center text-white">
        <h1 className="text-4xl font-semibold md:text-6xl">{homepage.hero_title}</h1>
        <p className="mt-4 max-w-2xl text-lg md:text-xl">{homepage.hero_subtitle}</p>
        {homepage.cta_link && homepage.cta_text ? (
          <a
            href={homepage.cta_link}
            className="mt-8 rounded-md bg-white px-6 py-3 font-medium text-black"
          >
            {homepage.cta_text}
          </a>
        ) : null}
      </div>
    </main>
  );
}

This is a good example of how AI can save time without taking away architectural ownership. You define the content model and helper layer. The assistant helps you move faster on implementation.

Step 10: handle rich text correctly

Rich text is one of the places where generic AI output often goes wrong. Contentstack rich text fields are stored as structured JSON, so you should use the appropriate utility package rather than treating them like plain HTML strings.

A reusable renderer can look like this:

// components/RichTextRenderer.tsx
import { jsonToHtml } from "@contentstack/utils";

export function RichTextRenderer({ json }: { json?: unknown }) {
  if (!json) return null;

  const html = jsonToHtml(json);

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

If you need stricter sanitization or custom rendering rules, you can extend this later. The important thing is to establish the correct baseline.

Step 11: handle images from Contentstack

Image fields typically return an object with a url property. In Next.js, you will usually want to pass that URL into next/image and allow the relevant Contentstack image host in your Next.js config.

import Image from "next/image";
export function ContentstackImage({
  src,
  alt,
}: {
  src?: string;
  alt: string;
}) {
  if (!src) return null;

  return (
    <Image
      src={`${src}?width=800`}
      alt={alt}
      width={800}
      height={450}
      className="h-auto w-full rounded-lg object-cover"
    />
  );
}