Ghost Content API Integration with Custom Frontend
Ghost Content API — public read-only REST API. Returns posts, pages, tags, authors, site settings. Used for headless scenarios: Next.js, Astro, Gatsby instead of native Handlebars themes.
Authentication and Keys
Content API Key created in Ghost Admin → Settings → Integrations → Add custom integration. Key passed as query param or header — it's public (read-only), safe to use on frontend.
// lib/ghost.ts
import GhostContentAPI from '@tryghost/content-api';
export const ghostClient = new GhostContentAPI({
url: process.env.GHOST_URL!, // https://myblog.com
key: process.env.GHOST_CONTENT_API_KEY!,
version: 'v5.0',
});
Core SDK Methods
// Posts list with pagination
const posts = await ghostClient.posts.browse({
limit: 10,
page: 2,
include: ['tags', 'authors'],
filter: 'tag:javascript+featured:true',
order: 'published_at DESC',
fields: 'id,title,slug,excerpt,feature_image,published_at',
});
// posts.meta.pagination: { page, limit, pages, total, next, prev }
// Single post by slug
const post = await ghostClient.posts.read(
{ slug: 'my-post-slug' },
{ include: ['tags', 'authors'], formats: ['html', 'plaintext'] }
);
// Pages (Pages)
const pages = await ghostClient.pages.browse({ limit: 'all' });
// Tags
const tags = await ghostClient.tags.browse({
limit: 'all',
include: 'count.posts',
order: 'count.posts DESC',
});
// Authors
const authors = await ghostClient.authors.browse({
include: 'count.posts',
});
// Site settings
const settings = await ghostClient.settings.browse();
Next.js App Router Integration
// app/blog/page.tsx
import { ghostClient } from '@/lib/ghost';
export const revalidate = 3600;
export default async function BlogPage() {
const posts = await ghostClient.posts.browse({
limit: 12,
include: ['tags', 'authors'],
});
return <PostGrid posts={posts} />;
}
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await ghostClient.posts.browse({ limit: 'all', fields: 'slug' });
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const post = await ghostClient.posts.read(
{ slug: params.slug },
{ include: ['tags', 'authors'] }
);
return <PostDetail post={post} />;
}
Rendering Ghost HTML
Ghost returns ready HTML in html field. For safe rendering and styling:
// components/GhostContent.tsx
import DOMPurify from 'isomorphic-dompurify';
export function GhostContent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['allowfullscreen', 'frameborder'],
});
return (
<div
className="ghost-content prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: clean }}
/>
);
}
CSS for Ghost card elements must be included separately:
// app/layout.tsx
import '@/styles/ghost-cards.css'; // styles for kg-bookmark, kg-gallery, kg-video, etc.
Members API for Headless
Members features (paywall, subscriptions) in headless mode require extra work. Ghost provides JS SDK @tryghost/portal as iframe widget — can embed in any frontend:
// Include Portal script
<script
src="https://myblog.com/public/member-attribution.min.js"
async
/>
// Open signup/login form
<button onClick={() => window.location.href = 'https://myblog.com/#/portal/signup'}>
Subscribe
</button>
Full headless integration with Members requires session proxying through Ghost — harder than native themes, usually not justified for publishing projects.
Integration Timeline
| Frontend | Task | Time |
|---|---|---|
| Next.js | Basic blog (list + post) | 1–2 days |
| Next.js | Full website (tags, authors, search) | 3–5 days |
| Astro | Static blog site | 1–2 days |
| Gatsby | With Source Plugin | 1–2 days |







