Ghost headless blog
Skill kasuncfdo/ghost-headless-blog-skill/skills/ghost-headless-blog
Implement a headless Ghost CMS blog (/blog) in a Next.js App Router site — Content API client, ISR + webhook revalidation, tag/author/paged archives, author bio + social rendering, SEO metadata + JSON-LD, sitemap, Ghost koenig-card styling, blur-up images. Use when adding a Ghost-powered blog to a Next.js project, or debugging an existing headless Ghost integration (empty blog, stale pages, broken images/cards).From its SKILL.md
npx -y skills add kasuncfdo/ghost-headless-blog-skill --skill ghost-headless-blogAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.9 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
Headless Ghost blog in Next.js (App Router)
Battle-tested patterns from a live production site (Next.js 16 / React 19 / Tailwind v4, Ghost Content API v6.0). Everything below was learned the hard way — follow the decisions, not just the code.
Architecture at a glance
- No
@tryghost/content-apidependency. Call the Content API REST endpoints directly ({GHOST_URL}/ghost/api/content/posts/?key=...) with anAccept-Version: v6.0header. The SDK adds weight and hides errors. - All Ghost fetching is server-side (server components, route handlers,
generateStaticParams, sitemap). Content API keys only expose public data, but keep them server-only anyway: env varsGHOST_URL/GHOST_CONTENT_API_KEYwith noNEXT_PUBLIC_prefix. - ISR everywhere + instant webhook purge. Every blog route exports
export const revalidate = 3600andexport const dynamicParams = true; a Ghost Admin webhook hits/api/revalidate?secret=...on post publish/update/unpublish/delete for instant purges. Hourly ISR is only the safety net. - Ghost post HTML is rendered verbatim via
dangerouslySetInnerHTMLinside<article className="gh-content">, styled by a dedicatedghost-content.css, with a small HTML post-processing pass (blur-up images, LCP fix) and tiny client components re-adding Ghost's interactive card JS (toggle cards).
Routes to build
| Route | Purpose |
|---|---|
/blog | Index: hero + feed. Only the Ghost-fetching part is an async component behind <Suspense> with a skeleton fallback. |
/blog/[slug] | Post page: metadata from Ghost SEO fields, BlogPosting JSON-LD, rendered gh-content, related posts. |
/blog/tag/[slug] | Tag archive (CollectionPage JSON-LD). Statically generated for crawlers even if the UI filters client-side. |
/blog/author/[slug] | Author archive: bio, avatar/cover, location, social links, post feed. ProfilePage + Person JSON-LD with sameAs socials. |
/blog/page/[page] | Paged feed archive; page 1 redirect("/blog"). |
/api/revalidate | Ghost webhook receiver → revalidatePath purges. |
sitemap.ts | Include posts (with real lastModified) + tag + author pages; Ghost outage must not break the sitemap (.catch(() => [])). |
Full route code + metadata/JSON-LD patterns: references/pages.md. Setup steps (env, Ghost Admin, next.config images, webhook): references/setup.md. Official Ghost docs lookup (llms-full.txt section-extraction workflow, Content API reference URLs): references/ghost-docs.md.
Copy-paste templates (portable, no project-specific deps)
- templates/ghost.ts — typed Content API client (posts, tags,
authors, slugs, related, featured, pagination, excerpt helpers,
authorSocialLinksnormalizer,toCardPostprojection) - templates/ghost-html.ts — blur-up + LCP HTML transform
- templates/revalidate-route.ts — webhook → ISR purge
- templates/ghost-content.css — full
.gh-contentprose + koenig card styles (dark palette; accent via--ghost-accent) - templates/components/BlurImage.tsx — next/image blur-up
- templates/components/ToggleCards.tsx — re-adds Ghost toggle-card JS
- templates/components/ReadingProgress.tsx — scroll progress bar
Non-negotiable decisions (each one fixed a real bug)
- Graceful degradation, three tiers (in
ghost.ts):- Env missing →
isGhostConfigured = false, every helper returns empty; build succeeds; one server-sideconsole.warn. UI shows a friendly "No posts yet" state. - Key rejected (401/403) → return
null/empty (config problem; warn once). A misconfigured deploy renders an empty blog instead of crashing. - Transient failure (network, 5xx) → throw. During ISR revalidation this keeps the previously rendered page instead of baking an empty page over good content.
- Env missing →
getPostBySluguses browse +filter=slug:x&limit=1, not thereadendpoint. A missing post is then an empty 200 instead of a 404 that retry logic hammers and rethrows; returnposts[0] ?? nullandnotFound()in the page.- Follow pagination (
meta.pagination.next,limit=100) when fetching all posts/slugs — Ghost caps page size; a single request silently truncates. toCardPostslim projection whenever many posts cross into a client component: striphtml, meta/og/twitter fields, author bios. Keeps the serialized RSC payload small (this mattered — full posts ballooned the page payload).- Webhook revalidates both
post.current.slugandpost.previous.slug— slugs can change on update. Also purge/blog,/blog/page/[page],/blog/tag/[slug]+/blog/author/[slug](with the"page"type arg), and/sitemap.xml. withBlurUpImagesHTML transform: inject inlineonloadhandlers (native HTML attrs — they work insidedangerouslySetInnerHTMLwithout hydration), and promote the first content image fromloading="lazy"toloading="eager" fetchpriority="high"— Ghost lazy-loads every image and the first is usually the LCP. AddsuppressHydrationWarningon the<article>because those handlers mutate classes before React hydrates.- next/image remote patterns: derive the Ghost hostname from
GHOST_URLat build time innext.config.mjs, plusstatic.ghost.organd the upload CDN (managed Ghost hosts like DigitalPress serve uploads from**.digitaloceanspaces.com). SetminimumCacheTTLlong (e.g. 31 days) — Ghost upload URLs are immutable. - Ghost SEO fields with fallbacks in
generateMetadata:meta_title || title,meta_description || excerpt(160),og_image || feature_image, honorcanonical_url. Typearticle+publishedTime/modifiedTime/authors/tagson posts. - Internal blog navigation must use
next/link(ornext-view-transitionsLink) — raw<a>tags (e.g. a navbar "Blog" tab) cause full page reloads. - Ghost cards need re-implementation client-side: Ghost's frontend JS isn't loaded,
so toggle cards need a click handler (
ToggleCards.tsx) and all.kg-*cards (callout, bookmark, button, gallery, embed, toggle, video) need CSS. Don't skip this — posts using those cards render broken otherwise. - Next 15+/16:
paramsis a Promise —const { slug } = await params;in pages andgenerateMetadata.
Known operational pitfalls
GHOST_CONTENT_API_KEYsilently missing (IDE overwriting.env.local, forgotten Vercel env): symptom is builds producing far fewer pages than expected and an empty blog. Check env first; the client's build-time warning log is the tell.- Custom Ghost domain moves: the next/image allowed host is derived from
GHOST_URLat build time, so hosting-provider env vars must be updated and the site rebuilt. - Ghost webhook "Secret" field: leave it empty — it sets an
X-Ghost-Signatureheader, not the query param. Pass the shared secret in the target URL (.../api/revalidate?secret=...) and compare againstGHOST_REVALIDATE_SECRET. - Canonical/OG base URL must not redirect: if the apex 307s to
www, some scrapers (e.g. Telegram og:image) choke. PointBASE_URLat the final (non-redirecting) host. - Never paste Admin API keys or the revalidate secret into chats/issues; rotate if exposed.
What ships with it: 10 files
54.0 KB alongside SKILL.md, 3 of them executable
references/
- ghost-docs.md2.6 KB
- pages.md11.2 KB
- setup.md4.5 KB
templates/
- components/BlurImage.tsx1.1 KB
- components/ReadingProgress.tsx1.4 KB
- components/ToggleCards.tsx1018 B
- ghost-content.css13.2 KB
- ghost-html.tsruns897 B
- ghost.tsruns16.2 KB
- revalidate-route.tsruns1.9 KB
Gives 0 of the 12 instructions most seo skills give in ~1.9k tokens
Counted across 342 of the 364 authors here whose files we hold, read 2026-09-06
- Read product marketing context before asking questionsin 38 of 342, across 16 files
- Use one clear H1 per pagein 25 of 342, across 13 files
- Measure Core Web Vitals against stated thresholdsin 23 of 342, across 16 files
- Verify robots.txt allows AI crawlersin 22 of 342, across 16 files
- Use descriptive anchor text for internal linksin 22 of 342, across 16 files
- Keep title tags around 50-60 charactersin 19 of 342, across 16 files
- Add a self-referencing canonical URL to every pagein 16 of 342, across 14 files
- Lead every section with a direct answerin 15 of 342, across 9 files
- Submit the sitemap to Google Search Consolein 15 of 342, across 12 files
- Keep key answer passages to 40-60 wordsin 14 of 342, across 8 files
- Add statistics with cited sourcesin 14 of 342, across 8 files
- Give each page one primary search intentin 13 of 342, across 7 files
Said here and by no other author read
- Call Ghost Content API endpoints directly, skipping the SDK
- Keep Ghost fetching and API keys server-side
- Use ISR everywhere with webhook-driven instant purges
- Fetch posts by slug via browse with limit one
- Follow pagination when fetching all posts
- Throw on transient Ghost fetch failures
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.