# Static Site Generation

- **Authors:** Tim Benniks, Lo Etheridge
- **Published:** 2026-04-02
- **Updated:** 2026-04-02T09:28:41.247Z
- **Tags:** headless, sdk, api, visual_editing

---

Prerequisites: This chapter builds on [How Live Preview Works ](/guides/the-ultimate-guide-to-contentstack-visual-building/how-live-preview-works)and shares concepts with [Server-Side Rendering](/guides/the-ultimate-guide-to-contentstack-visual-building/server-side-rendering). Understanding why preview requires runtime rendering helps you see why SSG needs an escape hatch.

What you'll be able to do after this chapter:

- Explain why SSG fundamentally conflicts with Live Preview and what the escape hatch is


- Configure Next.js Draft Mode or Astro hybrid rendering for preview


- Avoid the client-side patching antipattern and the hydration mismatches it causes



## Why this matters

Static files can't show drafts. Teams that try to patch static content client-side hit hydration mismatches and flicker. This chapter shows the framework-level escape hatches that make preview work cleanly.

SSG's core value (pre-built HTML served without runtime computation) directly conflicts with Live Preview's need for draft-aware, session-scoped content. Static files can't show drafts. The solution: framework-level preview modes that temporarily switch from static to dynamic rendering.

## The Fundamental Conflict

At build time, your generator fetches published content and renders HTML files. These files are deployed to a CDN. No server runs, no API calls fire per request. Live Preview needs runtime rendering of draft content - rebuilding and redeploying on every keystroke isn't practical.

## The Solution: Preview Mode

When preview mode is active, static files are bypassed and requests are handled dynamically (like SSR), fetching from the Preview API per request. When inactive, static files serve normally with no performance impact.

Conceptually, SSG preview is SSR with guardrails.

Aspect

Production (SSG)

Preview Mode

Rendering

Static files

Dynamic (SSR)

Content source

Built-in content

Preview API

Caching

Full CDN cache

No caching

Hash handling

Not applicable

Per-request


flowchart TB

  decision{"Preview mode active?"}

  production["Production (SSG)<br/>Build time fetch<br/>Static HTML<br/>CDN cached<br/>No draft content"]

  preview["Preview Mode<br/>Static files bypassed<br/>Dynamic render (SSR)<br/>Preview API + hash<br/>No caching"]


  decision -->|No| production

  decision -->|Yes| preview

## Framework Preview Modes

### Next.js (App Router with Draft Mode)

```typescript
// app/api/draft/route.js
import { draftMode } from 'next/headers';

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get('secret');
  const slug = searchParams.get('slug');

  if (secret !== process.env.PREVIEW_SECRET) {
    return new Response('Invalid token', { status: 401 });
  }

  // Cookie-backed flag: subsequent requests render dynamically and can use Preview API
  draftMode().enable();

  return new Response(null, {
    status: 307,
    headers: { Location: `/${slug}` }
  });
}

// app/[slug]/page.js
import { draftMode } from 'next/headers';

export default async function Page({ params }) {
  const { isEnabled } = draftMode();

  // Draft mode ON → behave like SSR for this request; OFF → use built static data path
  const data = isEnabled
    ? await fetchDraftContent(params.slug)
    : await fetchPublishedContent(params.slug);

  return <PageContent data={data} />;
}

export async function generateStaticParams() {
  const pages = await fetchAllPages();
  return pages.map(page => ({ slug: page.slug }));
}
```

### Next.js (Pages Router with Preview Mode)

```typescript
// pages/api/preview.js
export default function handler(req, res) {
  const { secret, slug } = req.query;

  if (secret !== process.env.PREVIEW_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  res.setPreviewData({}); // Sets preview cookies for the browser session
  res.redirect(`/${slug}`);
}

// pages/[slug].js
export async function getStaticProps({ params, preview }) {
  const data = preview
    ? await fetchDraftContent(params.slug)
    : await fetchPublishedContent(params.slug);

  return {
    props: { data },
    // Disable ISR caching while previewing so drafts are not frozen in edge cache
    revalidate: preview ? false : 60
  };
}

export async function getStaticPaths() {
  const pages = await fetchAllPages();
  return {
    paths: pages.map(page => ({ params: { slug: page.slug } })),
    fallback: 'blocking'
  };
}
```

### Astro (Hybrid Rendering)

Astro supports per-route SSR, making it straightforward:

```typescript
// astro.config.mjs
export default defineConfig({
  output: 'hybrid', // Allow some routes to opt into server rendering
});

// src/pages/[slug].astro
export const prerender = false; // This route is always server-rendered (needed for live_preview hash)

const { slug } = Astro.params;
const livePreviewHash = Astro.url.searchParams.get('live_preview');

const data = livePreviewHash
  ? await fetchDraftContent(slug, livePreviewHash)
  : await fetchPublishedContent(slug);
```

## The Common Mistake: Client-Side Patching

Don't try to "patch" static content with client-side refetching:

```typescript
// DON'T DO THIS
export async function getStaticProps({ params }) {
  const data = await fetchPublishedContent(params.slug);
  return { props: { data } };
}

function Page({ data }) {
  const [content, setContent] = useState(data);

  useEffect(() => {
    if (isPreviewMode()) {
      // Client replaces props after paint → HTML from build ≠ first client render
      fetchDraftContent().then(setContent);
    }
  }, []);

  return <Content data={content} />;
}
```

This fails because:

- Hydration mismatch: Server HTML has published content; client renders draft content


- Layout flicker: Published content flashes before draft content appears


- Inconsistent state: Some content updates, some doesn't



If you use SSG, accept that preview means temporarily leaving SSG. Don't patch static content dynamically.

## Integrating with Contentstack Live Preview

- Create a preview API route that enables preview mode and redirects


- Configure your stack's Live Preview Base URL to point to your preview endpoint


- Initialize the SDK client-side with ssr: true


- In preview mode, fetch from Preview API with the hash



## A Note on Contentstack's Official SSG Guidance

Contentstack's documentation states that SSG sites run Live Preview in CSR mode (ssr: false), fetching content dynamically in the browser rather than triggering iframe reloads.

The full picture: your framework's preview mode bypasses static files and renders dynamically. The Live Preview SDK runs in CSR mode within that dynamic context, fetching draft content and re-rendering in place. The framework handles "escape from static" and the SDK handles the "fetch draft content" loop.

If your SSG framework lacks a preview mode, you can run the SDK in CSR mode and fetch draft content client-side on top of the static page. This works but causes the hydration mismatch and flicker described above - use it as a fallback, not a primary strategy.

## Key Takeaways

- Static files can't show drafts. Use your framework's preview mode to temporarily switch to dynamic rendering.


- In preview mode, SSG pages behave like SSR: Preview API per request, no caching.


- Don't patch static content client-side. It causes hydration mismatches and flicker.


- The SDK runs in CSR mode (ssr: false) within the dynamic preview context.



## Check Your Understanding

- Why does client-side patching of static content cause a hydration mismatch? What specifically is different between the server-rendered HTML and the first client render?


- Your team uses Next.js with ISR (Incremental Static Regeneration). A developer proposes setting revalidate: 1 during preview to get near-real-time updates. Why is this insufficient for Live Preview?


- A colleague argues that since preview mode makes SSG pages behave like SSR, there's no performance difference. What's wrong with this reasoning in production?






---

## Frequently asked questions

### Why does static site generation conflict with Live Preview?

SSG renders published content at build time and serves static files from a CDN. Live Preview needs draft-aware, session-scoped content rendered at request time.

### What is the escape hatch for previewing drafts on an SSG site?

Use your framework’s preview mode to bypass static files and render dynamically (SSR) per request. Draft content is fetched from a Preview API using a per-session flag or hash.

### How do you enable draft preview in Next.js App Router?

Create an API route that validates a secret, calls draftMode().enable(), and redirects to the requested slug. In the page, branch fetching logic based on draftMode().isEnabled.

### Why is client-side patching of static content an antipattern for preview?

The HTML sent from the server contains published content, but the client replaces it with draft content after hydration. This causes hydration mismatches, layout flicker, and inconsistent state.

### How does Astro support Live Preview on otherwise static sites?

Configure output: 'hybrid' and set prerender = false on previewed routes. Those routes always run server-side and can fetch draft content using the live preview hash.



---

## Chapter Navigation

**← Previous:** [Server-Side Rendering](https://developers.contentstack.com/guides/the-ultimate-guide-to-contentstack-visual-building/server-side-rendering)

**Next →:** [Middleware and Database-Backed Architectures](https://developers.contentstack.com/guides/the-ultimate-guide-to-contentstack-visual-building/middleware-and-database-backed-architectures)
