Content Management

Ghost Headless CMS: Setup Guide for Developers

J
James Eriksson
··13 min read
Learn how to use Ghost as a headless CMS with Next.js and Astro. Complete guide to API keys, fetching posts, webhooks, and deployment. Compare to Strapi and Payload.
TL;DR
  • Ghost works as a headless CMS by exposing a REST API that your Next.js, Astro, or static generator consumes.
  • Set up is straightforward: install Ghost locally or use Ghost(Pro), generate a Content API key, and fetch posts in your frontend with the @tryghost/content-api npm package.
  • Webhooks automatically rebuild your static site when you publish; Ghost POSTs to your Netlify or Vercel deploy hook, no manual intervention needed.
  • You must build sitemaps, structured data, and author archives yourself; Ghost themes handle these automatically, but headless frontends require custom code.
  • Ghost excels at publishing and editing experience; Strapi and Payload offer more flexibility for custom data; Contentful scales to enterprise; choose based on your content model and team size.

Ghost works as a headless CMS by decoupling its REST API from any particular frontend. Your content lives in Ghost; your frontend lives in Next.js, Astro, or whatever you choose. This guide walks through API keys, fetching posts, triggering rebuilds with webhooks, and the honest limitations you'll face.

What Is Ghost as a Headless CMS?

In traditional Ghost, you write posts in the Ghost editor and they render on the Ghost theme. The backend and frontend are married. Headless Ghost flips this: Ghost is purely a backend content store. You generate an API key, fetch your posts over REST, and render them anywhere.

Ghost's Content API returns JSON: posts, authors, tags, and metadata. You consume this data in your frontend code. For JAMstack sites, this means you can generate static HTML at build time. For dynamic frontends, you fetch posts on demand. The @tryghost/content-api npm package handles authentication and query building, but the HTTP API works fine with plain fetch or axios.

The key architectural difference from traditional Ghost is that there's no "theme." Ghost stops caring how your content is displayed. This is why Ghost headless feels lean: you only get the editor, API, and member features. Publishing a post triggers a webhook (if configured), which can kick off a rebuild on Netlify or Vercel. Your static site stays fresh without manual intervention.

This decoupling is not unique to Ghost. Strapi, Payload, Contentful, and Sanity all do this. Ghost's angle is that it started as a publishing platform and kept the editorial experience smooth while adding API-first capability.

Why Use Ghost for Headless Publishing?

Ghost excels at one thing: publishing. The editor is fast. There's no configuration fatigue. You get a distraction-free writing interface, scheduling, revisions, and a built-in member system with subscriptions and paywalls. If your primary use case is content distribution, Ghost is overkill as a traditional monolithic CMS but it's exactly right as a headless backend.

The performance argument is real. One developer (Alex MacArthur) migrated from Notion to Ghost and reported a 9x faster build time. Notion's API is rate-limited and expensive to poll; Ghost's is optimized for bulk content fetches. This matters if your site is large or you rebuild frequently.

Ghost (Pro) includes webhooks out of the box, meaning you can trigger static site generators without writing a separate polling script. You push publish. A webhook fires. Your site rebuilds. No cron jobs. Self-hosted Ghost also supports webhooks; you just set up the URL manually.

The member system is the other win. Ghost manages subscriptions, paywalls, and email newsletters natively. If you want your site to have a "free tier plus paid subscribers" model, Ghost does the heavy lifting. Your frontend requests member-gated content; Ghost handles authentication. You don't build this yourself.

For small teams and solo developers, Ghost is pragmatic. It's not as flexible as Strapi (which lets you model any data structure) or as powerful as Contentful (which scales to enterprise complexity). But for blogs, publisher sites, and documentation where the content is mostly posts and pages, Ghost saves you weeks of setup.

Setting Up Ghost for Headless Mode

You have two paths: run Ghost locally for development and testing, or use Ghost(Pro) for production. Both work with headless frontends.

Self-hosted setup:

Install Node.js and npm, then use the Ghost CLI:

npm install -g ghost-cli@latest
ghost install local

This drops Ghost into a local folder with a SQLite database. You'll be prompted to create an admin user. Visit your Ghost instance's admin panel to log in. You now have a full Ghost instance running locally.

To set up headless mode, you don't change anything in Ghost's configuration. You just generate an API key. In the Ghost admin:

  1. Go to Settings > Integrations.
  2. Click "Add integration."
  3. Name it (e.g., "Next.js Frontend").
  4. Copy the "Content API Key."
  5. The API URL is your Ghost instance URL (http://localhost:2368 for local development, or your production domain if deployed).

That's it. You now have credentials to fetch posts. The Content API is read-only, which is fine for fetching published content. If you need to create or update posts from your frontend, you'd use a different auth mechanism or a backend service.

Test your API key by making a request to your Ghost instance's content API endpoint, passing your key as a query parameter. You'll get JSON with your posts. The Content API returns published posts, authors, tags, and excerpt (truncated). You don't get draft posts unless you're using the Admin API with a different key type.

For production, use a Ghost(Pro) instance (managed Ghost with automatic backups and scaling) or deploy self-hosted Ghost to a server or container. Opsily offers managed Ghost hosting, removing infrastructure management. You get the API and themes ready to use, and you focus on the frontend.

Fetching Posts from Ghost in Next.js/Astro

Next.js setup:

Install the Ghost SDK:

npm install @tryghost/content-api

Create a utility file to initialize the API client:

// lib/ghost-api.js
import GhostContentAPI from '@tryghost/content-api';

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

export default api;

Add environment variables to your .env.local:

GHOST_URL=http://localhost:2368
GHOST_KEY=your_content_api_key_here

Now fetch posts. For static generation, use getStaticProps and getStaticPaths:

// pages/blog/[slug].js
import api from '@/lib/ghost-api';

export async function getStaticPaths() {
  const posts = await api.posts.browse({
    limit: 'all',
    fields: 'slug'
  });
  
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    fallback: 'blocking'
  };
}

export async function getStaticProps({ params }) {
  const post = await api.posts.read(
    { slug: params.slug },
    { formats: 'html' }
  );
  
  return {
    props: { post },
    revalidate: 3600
  };
}

export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

This generates static pages at build time. When you publish a new post in Ghost, rebuild your Next.js site (via webhook, manually, or on a schedule) to include it.

Astro setup:

Astro's approach is similar but simpler (no getStaticProps needed):

// src/pages/blog/[slug].astro
import api from '@/lib/ghost-api.js';

export async function getStaticPaths() {
  const posts = await api.posts.browse({ limit: 'all' });
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
<article>
  <h1>{post.title}</h1>
  <p>{post.excerpt}</p>
  <div set:html={post.html} />
</article>

Astro compiles this at build time. No runtime overhead. The result is a flat HTML file served from a CDN.

Fetching multiple posts:

For a blog index:

const posts = await api.posts.browse({
  limit: 10,
  order: 'published_at DESC',
  include: 'authors,tags'
});

The include parameter adds author and tag metadata. The API supports filtering, searching, and sorting. Check the Ghost docs for the full query syntax.

Webhooks & Automatic Rebuilds

Manually rebuilding every time you publish is tedious. Webhooks automate this. When you publish a post in Ghost, a webhook fires and triggers a rebuild on your hosting platform.

Set up a webhook in Ghost:

  1. Go to Settings > Integrations > your integration.
  2. Scroll to "Webhooks."
  3. Click "Add webhook."
  4. Event: "Post published."
  5. Target URL: your Netlify or Vercel deploy hook URL (found in your platform's dashboard).

When you publish a post, Ghost POSTs to that URL. Your hosting platform receives it and kicks off a build. Your static site regenerates with the new post. No manual intervention.

If webhooks aren't available (self-hosted without setup), use polling: a cron job that asks Ghost for the latest posts every hour and rebuilds if something changed. This is slower and less elegant but works. Tools like GitHub Actions can run scheduled jobs for free.

Webhook payload:

Ghost sends JSON with post data:

{
  "post": {
    "id": "...",
    "slug": "my-new-post",
    "title": "My New Post",
    "status": "published"
  }
}

Your rebuild trigger doesn't need to parse this; it just needs to know something changed. Most deploy hooks ignore the body.

Limitations & Workarounds

Ghost headless mode drops some features that traditional Ghost themes handle automatically. Here's what breaks and how to fix it.

Sitemaps:

Traditional Ghost generates a sitemap at /sitemap.xml. Headless Ghost doesn't; there's no theme to generate it. You need to build it yourself.

In Next.js, create a dynamic route that fetches all posts and returns XML:

// pages/sitemap.xml.js
import api from '@/lib/ghost-api';

export default function Sitemap() {}

export async function getServerSideProps({ res }) {
  const posts = await api.posts.browse({ limit: 'all' });
  
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map(post => `
  <url>
    <loc>https://yoursite.com/blog/${post.slug}</loc>
    <lastmod>${new Date(post.updated_at).toISOString()}</lastmod>
  </url>
`).join('')}
</urlset>`;
  
  res.setHeader('Content-Type', 'application/xml');
  res.write(xml);
  res.end();
  
  return { props: {} };
}

Astro has a built-in sitemap integration that works similarly.

Structured data (JSON-LD):

Ghost themes auto-inject schema.org JSON-LD for SEO. You must add it manually:

const schema = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt,
  datePublished: post.published_at,
  author: {
    '@type': 'Person',
    name: post.authors[0].name
  }
};

Render this in your page's <head> as a <script> tag. Most Next.js starters use next-seo or next-head for convenience.

Author archives:

Traditional Ghost has /author/john/ URLs listing all posts by John. You need to build this:

  1. Fetch all posts with include: 'authors'.
  2. Group by author.
  3. Generate a page for each author listing their posts.

This is a frontend concern, not Ghost's problem.

Newsletter routing:

If Ghost is running newsletters and you're using headless frontends, members get the newsletter but the frontend site doesn't know about subscription status. You'd need:

  1. Ghost Admin API (different auth) to check member status.
  2. A backend service that validates member tokens.
  3. Conditional rendering in your frontend based on subscription.

This is complex and usually not worth it unless subscriptions are your core business model.

Image handling:

Ghost stores images in its own file system or external storage (S3, etc.). Your frontend gets image URLs from the API. If you move Ghost later, those URLs could break. Consider storing images in external storage from day one (S3, Cloudinary) to decouple them from Ghost.

Ghost Headless vs. Other Headless CMS

Five platforms compete in this space. None is universally better; each fits different needs.

Ghost

  • Best for: Publishing platforms, blogs, newsletters.
  • Editor experience: Excellent; distraction-free.
  • Data model: Posts, pages, authors, tags, members. Fixed.
  • API rate limits: Generous; no hard limits documented.
  • Price: Free self-hosted. Ghost(Pro) starts at $29/mo.
  • Learning curve: Low. Familiar if you've used Medium or Substack.

Strapi

  • Best for: Custom data models, e-commerce, headless + admin UI.
  • Editor experience: Functional but generic.
  • Data model: Completely custom; you define everything.
  • API rate limits: None enforced; depends on your server.
  • Price: Free self-hosted. Paid hosting starts at $2.50/mo.
  • Learning curve: Medium. Requires schema design and backend familiarity.

Payload

  • Best for: TypeScript teams, deeply custom backends.
  • Editor experience: Good; TypeScript-native.
  • Data model: Custom; defined in code.
  • API rate limits: None enforced.
  • Price: Free self-hosted. Paid hosting starts at $250/mo.
  • Learning curve: High. Requires TypeScript and full-stack thinking.

Contentful

  • Best for: Enterprise, multichannel delivery (web, mobile, email).
  • Editor experience: Industrial. Powerful but dense.
  • Data model: Custom; GraphQL and REST API.
  • API rate limits: Enforced per plan; can be restrictive.
  • Price: Free tier exists. Paid starts at $489/mo.
  • Learning curve: High. Enterprise tooling.

Sanity

  • Best for: Designers, highly structured content.
  • Editor experience: Customizable UI; can be beautiful or overwhelming.
  • Data model: Custom; GROQ query language.
  • API rate limits: Generous on free tier.
  • Price: Free tier. Paid starts at $99/mo.
  • Learning curve: Medium. GROQ syntax is a hurdle.

For a solo developer or small team building a blog or publisher site, Ghost wins on time-to-launch. You're writing and publishing within 10 minutes. For complex data (an e-commerce catalog, multiple content types), Strapi or Payload are more flexible. For enterprise teams needing audit logs and user management, Contentful is the default.

Frequently Asked Questions

Can I switch from headless Ghost back to traditional Ghost themes later?

Yes. Your posts, authors, and data live in Ghost regardless of how you access them. If you stop running your Next.js frontend and instead activate a Ghost theme, your content appears normally. You lose the SEO benefits of your custom frontend but no data is lost.

Do I need to rebuild my site every time I publish a post?

No, not if you use webhooks. When you hit publish in Ghost, a webhook automatically triggers a rebuild on Netlify, Vercel, or your CI/CD. The rebuild happens in the background (usually under 2 minutes). Your site updates without manual steps.

Can I use Ghost headless with multiple frontends simultaneously?

Yes. The same Ghost instance can serve content to a Next.js site, an iOS app, and an Astro documentation site. All three consume the same REST API. Content is written once, used everywhere. This is why headless CMS exists.

What happens if my API key is compromised?

The Content API key is read-only, so attackers can fetch your posts. That's public data anyway. Rotate the key (delete the old one, generate a new one) in Ghost settings. This breaks the old key immediately. The Admin API key is more sensitive (allows publishing), so keep it server-side and never expose it in client code.

Does Ghost headless support static site generation?

Yes. Tools like Next.js (getStaticProps), Astro, and Hugo can fetch Ghost content at build time and generate static HTML. This is the most performant approach: your site is flat files served from a CDN, no dynamic requests to Ghost at runtime.

Can I use Ghost headless with Ghost(Pro) or must I self-host?

Both work. Ghost(Pro) handles scaling and backups; you focus on the frontend. Self-hosted gives you full control and costs less but requires DevOps knowledge. For a small publisher, Ghost(Pro) + Netlify or Vercel (free tier) is the fastest path.

How do I handle member authentication in a headless setup?

Ghost(Pro) and self-hosted Ghost support the Member API for subscriber authentication. Your frontend can check membership status and conditionally render paywalled content. This requires a backend service that validates member tokens with Ghost; you can't do it purely client-side.

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

The Content API is read-only and public (safe to share the key). The Admin API allows creating, updating, and publishing posts; keep the key server-side. Ghost provides both; use the right one for your use case.

The Bottom Line

Ghost headless mode is ideal if you want a fast publishing platform without building a CMS from scratch. You get a professional editor, webhooks for rebuilds, and member/subscription support. The API is straightforward; connecting it to Next.js or Astro takes an hour.

The tradeoff is that you inherit the responsibility for sitemaps, structured data, and author archives. These are not hard problems, but they're not automated like in traditional Ghost themes.

For publishers, bloggers, and JAMstack projects, Ghost headless is pragmatic. For highly custom data models, Strapi is more flexible. For enterprise scale, Contentful is the incumbent. Choose based on your content structure and team size, not hype.

Ready to move from local testing to production? Opsily's managed Ghost hosting handles the infrastructure so you focus on the frontend and content. Get started at /hosting/ghost.

Stop managing Ghost yourself
Opsily handles the infrastructure. You focus on content and the frontend.
Get Started Free

Ready to self-host your own apps?

One server. Multiple apps. No per-app fees.

Get started →