How to Use Ghost with Astro: Complete Guide
Step-by-step guide to integrating Ghost with your Astro website.
Ghost is an open-source publishing platform that started as a blogging tool and has grown into a capable headless CMS. What makes Ghost interesting for Astro developers is that it comes with a well-designed Content API out of the box, memberships, newsletters, and a polished editor that writers actually enjoy using. You can self-host it for free or use Ghost(Pro) starting at $9/month.
The approach is straightforward: use Ghost as your content backend, pull posts through its Content API, and let Astro generate static pages. You get Ghost's excellent writing experience paired with Astro's performance.
Prerequisites
- Node.js 18+
- An Astro project (
npm create astro@latest) - A Ghost instance (self-hosted or Ghost(Pro) at $9/mo)
- A Content API key from your Ghost admin panel
Installation
Install the official Ghost Content API client:
npm install @tryghost/content-api
If you are using TypeScript, install the types as well:
npm install -D @types/tryghost__content-api
Configuration
Create a Ghost client helper:
// src/lib/ghost.ts
import GhostContentAPI from "@tryghost/content-api";
const ghost = new GhostContentAPI({
url: import.meta.env.GHOST_URL,
key: import.meta.env.GHOST_CONTENT_API_KEY,
version: "v5.0",
});
export default ghost;
Add your Ghost credentials to .env:
GHOST_URL=https://your-ghost-blog.com
GHOST_CONTENT_API_KEY=your_content_api_key_here
Find the Content API key in your Ghost admin panel under Settings > Integrations. Create a custom integration and copy the Content API Key. The URL is your Ghost site's root URL.
Basic Usage
Fetch all posts and render a blog index:
---
// src/pages/blog/index.astro
import ghost from "../../lib/ghost";
import BaseLayout from "../../layouts/BaseLayout.astro";
const posts = await ghost.posts.browse({
limit: "all",
include: ["tags", "authors"],
fields: ["id", "title", "slug", "excerpt", "feature_image", "published_at"],
});
---
<BaseLayout title="Blog">
<h1>Blog</h1>
{posts.map((post) => (
<article>
<a href={`/blog/${post.slug}`}>
{post.feature_image && (
<img src={post.feature_image} alt={post.title} loading="lazy" />
)}
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.published_at).toLocaleDateString()}</time>
</a>
</article>
))}
</BaseLayout>
Dynamic post pages with getStaticPaths:
---
// src/pages/blog/[slug].astro
import ghost from "../../lib/ghost";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const posts = await ghost.posts.browse({ limit: "all", fields: ["slug"] });
return posts.map((post) => ({
params: { slug: post.slug },
}));
}
const { slug } = Astro.params;
const post = await ghost.posts.read({ slug }, { include: ["tags", "authors"] });
---
<BaseLayout title={post.title}>
<article>
<h1>{post.title}</h1>
<div class="meta">
<span>By {post.authors?.[0]?.name}</span>
<time>{new Date(post.published_at).toLocaleDateString()}</time>
</div>
{post.feature_image && (
<img src={post.feature_image} alt={post.title} />
)}
<div class="content" set:html={post.html} />
<div class="tags">
{post.tags?.map((tag) => (
<a href={`/tag/${tag.slug}`}>{tag.name}</a>
))}
</div>
</article>
</BaseLayout>
You can also build tag and author pages by querying those endpoints:
// Fetch all tags
const tags = await ghost.tags.browse({ limit: "all", include: ["count.posts"] });
// Fetch posts by tag
const taggedPosts = await ghost.posts.browse({
filter: `tag:${tagSlug}`,
include: ["tags"],
});
Production Tips
Use Ghost's built-in image processing. Ghost stores images with size variants. Append
/size/w600/to image URLs for responsive images without an external image service. This works on both self-hosted and Ghost(Pro) instances.
Implement incremental builds. If you have hundreds of posts, querying all of them on every build gets slow. Use Ghost's filter parameter with updated_at to fetch only recently changed content, and cache the rest locally.
Set up a webhook for automatic rebuilds. In Ghost admin under Integrations, add a custom webhook that triggers your hosting platform's build hook whenever a post is published or updated.
Handle Ghost's HTML output carefully. Ghost returns pre-rendered HTML for post content. Style it with a dedicated .ghost-content CSS class rather than fighting with inline styles. Ghost's default output is clean, but you may want to add Tailwind typography plugin styles.
Use the fields parameter. When fetching post lists, only request the fields you need (title, slug, excerpt, feature_image). This reduces response size and speeds up builds, especially with large content libraries.
Alternatives to Consider
- Keystatic if you want a free, Git-based CMS that stores content directly in your repository with no external service.
- Strapi if you need a self-hosted CMS with more flexible content modeling and custom field types.
- Contentful if you want an enterprise-grade headless CMS with a larger ecosystem and more advanced content modeling.
Wrapping Up
Ghost and Astro complement each other well. Ghost gives you a polished writing experience with built-in memberships and newsletters, while Astro delivers the performance of a static site. The Content API is well-documented and the official JavaScript client handles pagination and filtering cleanly. For blogs and publications where the writing experience matters as much as the developer experience, this combination is hard to beat.
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.