# How to Integrate Ghost with V0

- Tool: V0
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: April 2026

## TL;DR

To use Ghost as a headless CMS with V0 by Vercel, fetch posts and pages from Ghost's Content API in your Next.js Server Components or API routes. V0 generates the blog listing and post page UI; your Next.js app fetches data from Ghost using your Content API key. Deploy to Vercel with ISR for fast, always-fresh blog pages.

## Ghost as a Modern Headless Blog Backend for V0 Apps

Ghost started as a WordPress alternative focused purely on blogging and newsletters. Over time it evolved into a powerful headless CMS with a clean, well-documented REST API — making it an excellent backend for Next.js frontends built with V0. The editorial experience is significantly better than WordPress for content creators: a distraction-free editor, built-in newsletter management, and membership/subscription tools. The technical experience for developers is equally clean: the Content API returns structured JSON with full post content, authors, tags, and metadata in a single request.

The integration architecture is simpler than most CMS setups because Ghost's Content API uses a public key — no OAuth flow, no server-side token exchange. You can fetch posts directly in React Server Components using the @tryghost/content-api SDK or raw fetch calls, and the data renders on the server before the page reaches the browser. This is the optimal pattern for SEO-focused blog pages: full HTML is available to crawlers, no loading states for content, and you can add ISR to automatically revalidate pages when new posts are published in Ghost.

For V0 users, the workflow is clean: generate the blog UI in V0 (post card grid, single post layout, author page), then wire the component's data props to Ghost API calls in Next.js page files. Your content team manages everything in Ghost's editor while the V0-generated frontend handles the presentation. This tutorial covers the complete setup from creating a Ghost Content API key to deploying a fast, SEO-ready blog on Vercel.

## Before you start

- A V0 account at v0.dev with an active project
- A Ghost instance — either Ghost(Pro) hosted plan at ghost.org or self-hosted Ghost on a VPS
- A Ghost Content API key created in Ghost Admin → Integrations → Add custom integration
- Your Ghost site's URL (e.g., https://yourblog.ghost.io or https://blog.yourdomain.com)
- A Vercel account for deployment — ISR and on-demand revalidation require Vercel or a compatible host

## Step-by-step guide

### 1. Create a Ghost Content API Key

Before writing any code, you need a Content API key from your Ghost instance. This is a public read-only key — unlike admin API keys, it only provides read access to published posts and pages, so it's safe to use in your Next.js Server Components without a server-side proxy. Log in to your Ghost Admin panel (usually at yourghost.io/ghost or yourdomain.com/ghost). Navigate to Settings → Integrations → Add custom integration. Give it a name like 'V0 Next.js Frontend' and click Create. Ghost generates a Content API key (a long alphanumeric string) and shows you the API URL. Copy both the Content API key and the API URL. The API URL is typically your Ghost site's root URL (e.g., https://yourblog.ghost.io) — Ghost appends /ghost/api/content/ to it automatically when you use the SDK. If you're self-hosting Ghost, make sure your Ghost instance is accessible from the internet (not just localhost) before proceeding, because Vercel's build process will fetch content from Ghost during the build phase. Also check that your Ghost version is 3.x or higher — the Content API was significantly improved in Ghost 3.0 and the @tryghost/content-api SDK targets version 3+.

**Expected result:** You have a Ghost Content API key (format: a long hex string) and your Ghost site URL. Both are ready to be added to your Next.js environment variables.

### 2. Generate the Blog UI in V0

Open V0 and describe the blog layouts you need. For a typical Ghost-powered blog, you'll need at least two page layouts: a post listing page (home or /blog) and a single post page (/blog/[slug]). Prompt V0 for each layout separately to get focused, production-quality results. For the listing page, describe the post card component with the data fields Ghost returns: title, feature_image (cover photo URL), custom_excerpt or excerpt, primary_author (author object with name and profile_image), primary_tag, published_at (date string), and reading_time (number, minutes). For the single post page, describe the full article layout including where the post body HTML will be injected. Ask V0 to use a clean, readable typography system — Ghost posts often contain code blocks, pull quotes, and image galleries, so the stylesheet needs to handle varied content types. In V0's Design Mode, adjust the typography and spacing to match your brand. The post body HTML that Ghost returns includes its own class names (kg-card for Koenig editor blocks, kg-image, kg-gallery-card, etc.) — you'll want to add CSS for these in your global stylesheet. Ask V0 to generate placeholder content that mimics real blog posts, including fake images, author names, and multi-paragraph excerpts, so you can evaluate the layout before connecting real Ghost data.

**Expected result:** V0 generates a blog listing component and a post page layout component with proper data shape props. The layouts handle varied content lengths and include loading and empty states.

### 3. Install the Ghost Content API SDK and Fetch Posts

Install the @tryghost/content-api package, then create a Ghost API client module at lib/ghost.ts. This module initializes the Ghost Content API client with your site URL and Content API key, and exports helper functions for fetching posts, pages, tags, and authors. In your Next.js page files, import these helpers and call them in Server Components (no 'use client' directive) to fetch Ghost data at request time or build time. The @tryghost/content-api SDK returns typed response objects for posts, pages, tags, and authors. Each post object includes fields like id, title, slug, html (rendered post content), feature_image, custom_excerpt, excerpt, published_at, reading_time, primary_author, primary_tag, and tags. For the post listing page, call api.posts.browse({ limit: 12, include: ['authors', 'tags'] }) to get the first 12 posts with full author and tag data. For a single post page, call api.posts.read({ slug: params.slug }, { include: ['authors', 'tags'] }) to get a specific post by slug. Wrap the API calls in a try/catch and return null on error — Next.js not-found.tsx handles the 404 case gracefully. One important note about TypeScript: the @tryghost/content-api package has TypeScript type definitions but they're not always perfectly accurate for all Ghost versions. If you encounter type errors, you can create a local types file or use type assertions.

```
import GhostContentAPI from '@tryghost/content-api';

const api = new GhostContentAPI({
  url: process.env.GHOST_URL!,
  key: process.env.GHOST_CONTENT_API_KEY!,
  version: 'v5.0',
});

export interface GhostPost {
  id: string;
  title: string;
  slug: string;
  html: string;
  feature_image: string | null;
  custom_excerpt: string | null;
  excerpt: string;
  published_at: string;
  reading_time: number;
  primary_author: { name: string; profile_image: string | null; slug: string };
  primary_tag: { name: string; slug: string } | null;
}

export async function getPosts(page = 1, limit = 12): Promise<GhostPost[]> {
  try {
    return await api.posts.browse({
      limit,
      page,
      include: ['authors', 'tags'],
      order: 'published_at DESC',
    }) as unknown as GhostPost[];
  } catch (error) {
    console.error('Ghost API error:', error);
    return [];
  }
}

export async function getPost(slug: string): Promise<GhostPost | null> {
  try {
    return await api.posts.read(
      { slug },
      { include: ['authors', 'tags'] }
    ) as unknown as GhostPost;
  } catch (error) {
    return null;
  }
}

export async function getTags() {
  try {
    return await api.tags.browse({ limit: 'all', include: ['count.posts'] });
  } catch {
    return [];
  }
}
```

**Expected result:** lib/ghost.ts exports working API helper functions. Calling getPosts() returns a typed array of Ghost post objects. The Next.js pages can import and call these functions in Server Components.

### 4. Build the Next.js Blog Pages with ISR

Create the actual Next.js page files that wire V0's UI components to Ghost API data. For the blog listing page at app/blog/page.tsx, create a Server Component that calls getPosts() and passes the data to your V0-generated listing component. For the individual post page at app/blog/[slug]/page.tsx, call getPost(params.slug) and pass the post data to your V0-generated post layout. The most important addition here is Incremental Static Regeneration (ISR) — configure your page with revalidate to automatically regenerate cached pages after a set interval. A revalidate of 3600 (1 hour) means Ghost post updates will appear on your site within an hour of publishing. For immediate updates, add a Ghost webhook that triggers on-demand revalidation via a Next.js revalidatePath API route. To render Ghost's HTML post content, use dangerouslySetInnerHTML with DOMPurify for sanitization. Ghost's HTML output is from a trusted source (your own CMS), but sanitizing prevents XSS if someone compromised your Ghost admin. Add a global stylesheet for Ghost's Koenig editor CSS classes — the kg-card, kg-image, kg-gallery-card, and kg-bookmark-card classes need styling to look correct. For generateStaticParams, return all post slugs at build time to pre-render every post as a static page.

```
import { notFound } from 'next/navigation';
import { getPost, getPosts } from '@/lib/ghost';

export const revalidate = 3600; // Revalidate every hour

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props) {
  const post = await getPost(params.slug);
  if (!post) return {};
  return {
    title: post.title,
    description: post.custom_excerpt || post.excerpt,
    openGraph: {
      images: post.feature_image ? [post.feature_image] : [],
    },
  };
}

export async function generateStaticParams() {
  const posts = await getPosts(1, 'all' as unknown as number);
  return posts.map((post) => ({ slug: post.slug }));
}

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

  return (
    <article className="max-w-2xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <div className="flex items-center gap-3 text-sm text-gray-500 mb-8">
        <span>{post.primary_author.name}</span>
        <span>·</span>
        <span>{new Date(post.published_at).toLocaleDateString()}</span>
        <span>·</span>
        <span>{post.reading_time} min read</span>
      </div>
      {post.feature_image && (
        <img src={post.feature_image} alt={post.title} className="w-full rounded-lg mb-8" />
      )}
      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.html }}
      />
    </article>
  );
}
```

**Expected result:** Visiting /blog shows a server-rendered post listing fetched from Ghost. Visiting /blog/some-post-slug renders the full post with Ghost's HTML content. Pages are cached by ISR and automatically revalidate after one hour.

### 5. Configure Environment Variables and Deploy

Ghost requires two environment variables: GHOST_URL (your Ghost site's root URL, e.g., https://yourblog.ghost.io — no trailing slash) and GHOST_CONTENT_API_KEY (the public Content API key from Ghost Admin → Integrations). Both are safe to use in server-side code. The GHOST_CONTENT_API_KEY is technically public (it only allows reading published content) but convention is to keep it as a server-only variable without NEXT_PUBLIC_ prefix to avoid unnecessarily exposing it. Add both variables in Vercel Dashboard → Settings → Environment Variables, selecting the Production and Preview environments. After adding variables, push your code to GitHub and Vercel will auto-deploy. For immediate Ghost-to-Vercel updates (instead of waiting for ISR), set up a Ghost webhook: go to Ghost Admin → Integrations → your integration → Add webhook. Set the event to 'Post published' and the URL to https://your-vercel-app.vercel.app/api/revalidate?secret=YOUR_SECRET&path=/blog. Create the /api/revalidate route in Next.js using revalidatePath from next/cache. For complex Ghost setups with custom membership integrations, multiple publications, or Ghost's newsletter system, RapidDev can help structure the Next.js routing and ISR configuration for optimal performance.

```
# .env.local
# Ghost CMS credentials
GHOST_URL=https://yourblog.ghost.io
GHOST_CONTENT_API_KEY=your_content_api_key_here

# Optional: secret for on-demand ISR revalidation webhook
REVALIDATE_SECRET=your_random_secret_here
```

**Expected result:** All environment variables are set in Vercel. The deployed blog fetches and renders Ghost posts. New posts published in Ghost appear on the site within the ISR revalidation window, or immediately if you've set up the webhook.

## Best practices

- Use Next.js generateStaticParams to pre-render all blog post pages at build time — Ghost content is rarely updated mid-session and static rendering gives the best performance and SEO
- Add the @tailwindcss/typography plugin and prose class to your Ghost HTML container for automatic styling of all content types including code blocks, blockquotes, and images
- Set up a Ghost webhook to trigger on-demand ISR revalidation when posts are published — this gives you instant publishing without waiting for ISR timeout intervals
- Keep GHOST_CONTENT_API_KEY as a server-only variable (no NEXT_PUBLIC_ prefix) even though it's a public read key — this prevents unnecessary exposure in the browser bundle
- Use Ghost's custom_excerpt field for meta descriptions and card excerpts rather than auto-generated excerpt — custom excerpts are written by authors specifically for preview contexts
- Handle null feature_image gracefully — not all Ghost posts have cover images, and Next.js Image with a null src throws a build error

## Use cases

### Headless Blog with Ghost Editor + V0 Frontend

Run Ghost for its superior editorial experience while using a V0-generated Next.js frontend for the public-facing blog. Content creators write and publish in Ghost's clean editor; the Next.js ISR pages serve the content at CDN speed. This combination is popular for startup blogs, SaaS marketing sites, and personal publications.

Prompt example:

```
Create a blog homepage with a featured post hero section at the top (large image, title, excerpt, author) and a 3-column grid of recent post cards below it. Each card shows the post's feature image, title, excerpt (truncated at 120 characters), author name, and publication date. Include a 'Load More' button at the bottom that fetches the next page of posts.
```

### Individual Post Page with Ghost's HTML Content

Render individual blog posts using Ghost's pre-rendered HTML content. Ghost formats post content as clean HTML that you can safely inject into your Next.js page using dangerouslySetInnerHTML with sanitization. Add reading time, table of contents, author bio, and related posts to create a full editorial page layout.

Prompt example:

```
Build a blog post page layout with a centered content column (max-width 720px). Include the post title in a large h1, author name and avatar on the left, publication date on the right, and an estimated reading time badge. Below the hero, add a content area where the post HTML will be rendered. At the bottom, show an author bio card and a 'More from this author' section with 3 post cards.
```

### Tag and Author Archive Pages

Build archive pages that list all posts for a specific tag or author. Ghost's Content API supports filtering posts by tag slug or author slug, making it straightforward to create category and author landing pages. These pages are valuable for SEO and help readers navigate a large post library.

Prompt example:

```
Create a tag archive page that shows the tag name as a large heading, a short tag description below it, and then a grid of post cards filtered by that tag. Each card shows the featured image, title, excerpt, and date. Add a breadcrumb navigation at the top: Home > Blog > {Tag Name}.
```

## Troubleshooting

### API returns 'Unknown Content API Key' or 401 error on all Ghost API calls

Cause: The GHOST_CONTENT_API_KEY environment variable is missing, incorrect, or the key was created for the wrong Ghost integration type (Admin API keys and Content API keys are different).

Solution: Verify your key in Ghost Admin → Integrations. The Content API key is labeled 'Content API key' and is shorter than the Admin API key. Copy it exactly — no leading or trailing spaces. If using Ghost(Pro), ensure your Ghost site is on version 3.0 or higher.

### Blog posts render with broken layout — no images, unstyled code blocks, or missing formatting

Cause: Ghost's Koenig editor generates HTML with specific CSS class names (kg-card, kg-image-card, kg-code-card) that require Ghost's stylesheet to render correctly.

Solution: Add the @tailwindcss/typography plugin and apply the prose class to your post content container. For Ghost-specific Koenig card styles, add a ghost-content.css file with Ghost's official Koenig CSS from https://github.com/TryGhost/Ghost/tree/main/ghost/core/core/frontend/src/cards/css

```
// tailwind.config.ts
module.exports = {
  plugins: [require('@tailwindcss/typography')],
};

// In your post page component:
<div className="prose prose-lg max-w-none" dangerouslySetInnerHTML={{ __html: post.html }} />
```

### Build fails with 'fetch failed' or 'ECONNREFUSED' during next build on Vercel

Cause: Vercel's build process cannot reach your Ghost instance. Either the GHOST_URL is incorrect, your self-hosted Ghost is behind a firewall, or the Ghost server is down.

Solution: Verify your Ghost URL is publicly accessible by opening it in a browser. For self-hosted Ghost, ensure port 443/80 is open and Ghost is running. Check Vercel's build logs for the specific URL that failed. If you have generateStaticParams, add error handling so a failed Ghost fetch returns an empty array rather than crashing the build.

```
export async function generateStaticParams() {
  try {
    const posts = await getPosts();
    return posts.map((post) => ({ slug: post.slug }));
  } catch {
    return []; // Build succeeds even if Ghost is unreachable
  }
}
```

## Frequently asked questions

### Does Ghost work with V0's preview environment on Vercel?

Yes — Ghost's Content API is just a public HTTP endpoint, so Vercel preview deployments can fetch from it the same as production. Just make sure GHOST_URL and GHOST_CONTENT_API_KEY are set for the Preview environment in Vercel Dashboard → Settings → Environment Variables, not just Production.

### Can I use Ghost's built-in newsletter with a V0 Next.js frontend?

Ghost's newsletter feature sends emails to members directly from Ghost — this works independently of your frontend. Your V0 app only handles the public-facing blog pages. For membership sign-up, use Ghost's hosted portal (/portal) or integrate Ghost's Member API to create custom sign-up forms that connect to Ghost members.

### How do I handle Ghost's membership-gated content in Next.js?

Ghost's Content API only returns content accessible with the public key — gated member-only content is not returned. To show teaser content with a sign-in wall, fetch the post's excerpt from the Content API and redirect authenticated members to Ghost's hosted portal for full content access. Full custom member authentication requires Ghost's Admin API and Member API, which is more complex.

### What's the difference between Ghost's Content API and Admin API?

The Content API is read-only and uses a public key — it's designed for frontends and returns published posts, pages, tags, and authors. The Admin API has full read/write access and uses a secret key — it's for backend integrations like importing posts, managing members, or triggering newsletters. For a V0 blog frontend, you only need the Content API.

### How do I migrate from WordPress to Ghost for a V0 project?

Ghost has a built-in WordPress importer in Ghost Admin → Labs → Import. It converts WordPress posts, pages, tags, and images to Ghost's format. After importing, create a new Content API key and point your V0 Next.js app at Ghost instead of the WordPress REST API. The main difference in your code is the SDK — replace the WP REST API calls with @tryghost/content-api.

---

Source: https://www.rapidevelopers.com/v0-integrations/ghost
© RapidDev — https://www.rapidevelopers.com/v0-integrations/ghost
