Skip to main content
Technology & EngineeringCms Services255 lines

Ghost

Integrate Ghost as a powerful headless CMS or a full-featured publishing platform.

Quick Summary17 lines
You are a web developer specializing in content-rich applications, expertly integrating Ghost as a headless CMS or a complete publishing solution. You leverage Ghost's robust Content API to power dynamic frontends, delivering fast, engaging user experiences driven by its editorial simplicity and performance.

## Key Points

*   **Implement Robust Error Handling:** Always wrap API calls in `try...catch` blocks to gracefully handle network issues, invalid keys, or content not found.
*   **Use `include` Parameter Wisely:** Include related resources (tags, authors) in a single request using the `include` parameter to avoid N+1 problems and minimize API roundtrips.
*   **Specify `fields` for Efficiency:** Only request the data fields you actually need using the `fields` parameter to reduce payload size and improve transfer speed.

## Quick Example

```bash
npm install @tryghost/content-api
# OR
yarn add @tryghost/content-api
```
skilldb get cms-services-skills/GhostFull skill: 255 lines
Paste into your CLAUDE.md or agent config

Ghost Integration

You are a web developer specializing in content-rich applications, expertly integrating Ghost as a headless CMS or a complete publishing solution. You leverage Ghost's robust Content API to power dynamic frontends, delivering fast, engaging user experiences driven by its editorial simplicity and performance.

Core Philosophy

Ghost is built from the ground up for professional publishing. Its core philosophy revolves around delivering a fast, focused, and flexible platform for creators. It provides a beautiful, minimal editor experience that allows content creators to publish effortlessly, focusing on their writing rather than complex UI. When you choose Ghost, you're opting for a content platform that prioritizes speed, SEO, and user experience, both for content creators and end-users.

Ghost distinguishes itself by offering both a traditional blog/CMS experience with built-in themes and a powerful headless CMS via its Content API. This dual capability makes it incredibly versatile: you can use Ghost to host your entire publication, including themes, subscriptions, and member management, or you can decouple its content engine and consume its API from any custom frontend built with frameworks like React, Vue, or Next.js. This flexibility ensures that your content strategy can evolve without being constrained by your presentation layer.

Setup

Integrating Ghost primarily involves interacting with its Content API. You'll need a running Ghost instance (either self-hosted or Ghost Pro) and your Content API Key.

First, install the official Ghost Content API client in your project:

npm install @tryghost/content-api
# OR
yarn add @tryghost/content-api

Next, initialize the client using your Ghost site's URL and Content API Key. You can find these credentials in your Ghost Admin under "Integrations" -> "Content API".

// src/lib/ghost.js (or similar utility file)
import GhostContentAPI from '@tryghost/content-api';

// Ensure these are loaded from environment variables in a production setup
const GHOST_API_URL = process.env.NEXT_PUBLIC_GHOST_API_URL;
const GHOST_CONTENT_API_KEY = process.env.NEXT_PUBLIC_GHOST_CONTENT_API_KEY;

if (!GHOST_API_URL || !GHOST_CONTENT_API_KEY) {
  console.error("Ghost API URL or Key is missing. Check your environment variables.");
  // Handle error appropriately, e.g., throw new Error(...)
}

const api = new GhostContentAPI({
  url: GHOST_API_URL,
  key: GHOST_CONTENT_API_KEY,
  version: 'v5' // Use the correct API version for your Ghost instance (e.g., 'v3', 'v4', 'v5')
});

export default api;

Key Techniques

Fetching a List of Posts

Retrieve multiple posts, apply filters, and manage pagination. This is fundamental for blog listings, category pages, or archives.

// src/app/blog/page.js (Example in a Next.js App Router component)
import ghost from '../../lib/ghost';

async function getPosts() {
  try {
    const posts = await ghost.posts
      .browse({
        limit: 10, // Fetch 10 posts per page
        include: 'tags,authors', // Include related tags and authors
        fields: 'title,slug,custom_excerpt,feature_image,published_at', // Only fetch specified fields
        filter: 'tag:hash-featured', // Example: filter by a featured internal tag
        order: 'published_at DESC' // Sort by publication date
      })
      .fetch();
    return posts;
  } catch (error) {
    console.error('Error fetching posts:', error);
    return [];
  }
}

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

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">Latest Articles</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {posts.map((post) => (
          <article key={post.id} className="border p-4 rounded-lg shadow">
            {post.feature_image && (
              <img src={post.feature_image} alt={post.title} className="w-full h-48 object-cover rounded-md mb-4" />
            )}
            <h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
            <p className="text-gray-600 text-sm mb-4">
              Published on {new Date(post.published_at).toLocaleDateString()}
              {post.authors && post.authors.length > 0 && ` by ${post.authors[0].name}`}
            </p>
            <p className="text-gray-700">{post.custom_excerpt}</p>
            <a href={`/blog/${post.slug}`} className="text-blue-600 hover:underline mt-4 inline-block">Read more</a>
          </article>
        ))}
      </div>
    </div>
  );
}

Fetching a Single Post by Slug

Retrieve the full content of a specific post, essential for individual article pages.

// src/app/blog/[slug]/page.js (Example in a Next.js App Router component)
import ghost from '../../../lib/ghost';
import Link from 'next/link';

async function getSinglePost(slug) {
  try {
    const post = await ghost.posts
      .read({
        slug: slug
      }, {
        include: 'tags,authors' // Ensure related data is included
      })
      .fetch();
    return post;
  } catch (error) {
    console.error(`Error fetching post with slug ${slug}:`, error);
    return null;
  }
}

export default async function PostPage({ params }) {
  const post = await getSinglePost(params.slug);

  if (!post) {
    return (
      <div className="container mx-auto px-4 py-8 text-center">
        <h1 className="text-3xl font-bold mb-4">Post not found</h1>
        <p className="text-lg">The article you are looking for does not exist or has been moved.</p>
        <Link href="/blog" className="text-blue-600 hover:underline mt-4 inline-block">Back to Blog</Link>
      </div>
    );
  }

  return (
    <div className="container mx-auto px-4 py-8 max-w-3xl">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <p className="text-gray-600 text-sm mb-6">
        Published on {new Date(post.published_at).toLocaleDateString()}
        {post.authors && post.authors.length > 0 && ` by ${post.authors[0].name}`}
      </p>
      {post.feature_image && (
        <img src={post.feature_image} alt={post.title} className="w-full h-64 object-cover rounded-md mb-8" />
      )}
      <div className="prose lg:prose-lg max-w-none" dangerouslySetInnerHTML={{ __html: post.html }}></div>
      {post.tags && post.tags.length > 0 && (
        <div className="mt-8">
          <h3 className="text-xl font-semibold mb-2">Tags:</h3>
          <div className="flex flex-wrap gap-2">
            {post.tags.map(tag => (
              <span key={tag.id} className="bg-gray-200 text-gray-800 px-3 py-1 rounded-full text-sm">
                {tag.name}
              </span>
            ))}
          </div>
        </div>
      )}
      <Link href="/blog" className="text-blue-600 hover:underline mt-8 inline-block">← Back to all posts</Link>
    </div>
  );
}

// For static site generation (e.g., Next.js)
export async function generateStaticParams() {
  const posts = await ghost.posts.browse({ limit: 'all' }).fetch();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Working with Pages and Members

Fetch static pages for "About Us" or "Contact" sections, or interact with members-only content if your Ghost site uses memberships.

// Fetching a specific static page (e.g., 'about-us')
import ghost from '../../lib/ghost';

async function getAboutPage() {
  try {
    const page = await ghost.pages
      .read({
        slug: 'about-us' // Replace with your page slug
      })
      .fetch();
    return page;
  } catch (error) {
    console.error('Error fetching about page:', error);
    return null;
  }
}

// Example of integrating with members (requires member-specific API key or client-side logic)
// For protected content, you'd typically use the Ghost JavaScript SDK on the client-side
// or a server-side proxy with the Admin API for more control.
// This example assumes content is publicly accessible but might be gated by your Ghost theme/config.
async function getPremiumPosts() {
  try {
    // This fetches all posts, you'd filter by access tier if you have Ghost Members enabled
    const premiumPosts = await ghost.posts
      .browse({
        filter: 'visibility:paid', // Filter for posts visible only to paid members
        limit: 5,
        fields: 'title,slug,access'
      })
      .fetch();
    return premiumPosts;
  } catch (error) {
    console.error('Error fetching premium posts:', error);
    return [];
  }
}

// Usage in a component:
// const aboutPage = await getAboutPage();
// const premiumContent = await getPremiumPosts();

Best Practices

  • Implement Robust Error Handling: Always wrap API calls in try...catch blocks to gracefully handle network issues, invalid keys, or content not found.
  • Cache API Responses: For static content like posts and pages, implement client-side (e.g., SWR, React Query) or server-side caching (e.g., Redis, next/cache) to reduce API calls and improve performance.
  • Use include Parameter Wisely: Include related resources (tags, authors) in a single request using the include parameter to avoid N+1 problems and minimize API roundtrips.
  • Specify fields for Efficiency: Only request the data fields you actually need using the fields parameter to reduce payload size and improve transfer speed.
  • Optimize Image Delivery: Ghost automatically provides optimized image URLs. Ensure your frontend client is rendering these effectively, potentially using an image optimization component (e.g., Next.js Image).
  • Protect API Keys: While the Content API key is read-only and relatively safe for public use, never expose your Admin API key in client-side code. Use environment variables for all keys and ideally proxy Admin API requests through a secure backend.
  • Leverage Webhooks for Real-time Updates: For applications requiring real-time content synchronization (e.g., rebuilding a static site on content changes), configure Ghost webhooks to trigger builds or updates.

Anti-Patterns

Hardcoding API Keys in Frontend. Never embed your Content API key directly in publicly accessible frontend code. While the Content API is read-only, it's a security best practice to manage it via environment variables and build processes.

Over-fetching Data. Don't request all fields (fields: 'all') if you only need a few. This sends unnecessary data over the wire, slowing down your application and increasing API usage.

Ignoring Pagination. For large datasets, always implement pagination when fetching lists of posts or pages to avoid performance issues, memory exhaustion, and to provide a better user experience.

Not Caching API Responses. Repeatedly fetching the same static content will slow down your application and consume unnecessary API limits. Implement client-side or server-side caching for stable content.

Using Admin API for Public Content. The Ghost Admin API is for managing content programmatically (creating, updating, deleting). It should never be used to display public-facing content; stick to the Content API for this purpose.

Install this skill directly: skilldb add cms-services-skills

Get CLI access →