Ghost headless blog
Skill kasuncfdo/ghost-headless-blog-skill/skills/ghost-headless-blog
Claude Code skill: headless Ghost CMS blog for Next.js App Router — Content API client, ISR + webhook revalidation, SEO, koenig-card styling
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.
2 things to look at
- 23 days oldThe repository was created 23 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 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.
What its author says it does
Copied from the file, not written here
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).
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.
Gives 0 of the 12 instructions most seo skills give in ~1.9k tokens
Counted across 454 of the 460 authors here whose files we hold, read 2026-08-06
- implement structured data using JSON-LDin 30 of 454, across 26 files
- write unique meta descriptions under 160 charactersin 27 of 454, across 20 files
- verify one H1 exists per pagein 24 of 454, across 15 files
- maintain a single h1 per pagein 24 of 454, across 15 files
- use JSON-LD format for all schema markupin 23 of 454, across 15 files
- use descriptive anchor text for internal linksin 21 of 454, across 16 files
- add descriptive alt text to imagesin 19 of 454, across 15 files
- read product marketing context before auditingin 19 of 454, across 10 files
- write unique title tags under 60 charactersin 19 of 454, across 14 files
- add unique title and meta description per pagein 19 of 454, across 17 files
- Reference the sitemap in robots.txtin 19 of 454, across 18 files
- verify core web vitals meet thresholdsin 17 of 454, across 9 files
Said here and by no other author read
- call the content api rest endpoints directly
- keep api keys server-side only
- export revalidate and dynamicparams on every blog route
- render ghost post html verbatim
- revalidate both current and previous post slugs
- purge index tag author and page routes on webhook
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.