agentsclimarketplace

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

Install
npx -y skills add kasuncfdo/ghost-headless-blog-skill --skill ghost-headless-blog

Assembled 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-api dependency. Call the Content API REST endpoints directly ({GHOST_URL}/ghost/api/content/posts/?key=...) with an Accept-Version: v6.0 header. 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 vars GHOST_URL / GHOST_CONTENT_API_KEY with no NEXT_PUBLIC_ prefix.
  • ISR everywhere + instant webhook purge. Every blog route exports export const revalidate = 3600 and export 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 dangerouslySetInnerHTML inside <article className="gh-content">, styled by a dedicated ghost-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

RoutePurpose
/blogIndex: 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/revalidateGhost webhook receiver → revalidatePath purges.
sitemap.tsInclude 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)

Non-negotiable decisions (each one fixed a real bug)

  1. Graceful degradation, three tiers (in ghost.ts):
    • Env missing → isGhostConfigured = false, every helper returns empty; build succeeds; one server-side console.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.
  2. getPostBySlug uses browse + filter=slug:x&limit=1, not the read endpoint. A missing post is then an empty 200 instead of a 404 that retry logic hammers and rethrows; return posts[0] ?? null and notFound() in the page.
  3. Follow pagination (meta.pagination.next, limit=100) when fetching all posts/slugs — Ghost caps page size; a single request silently truncates.
  4. toCardPost slim projection whenever many posts cross into a client component: strip html, meta/og/twitter fields, author bios. Keeps the serialized RSC payload small (this mattered — full posts ballooned the page payload).
  5. Webhook revalidates both post.current.slug and post.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.
  6. withBlurUpImages HTML transform: inject inline onload handlers (native HTML attrs — they work inside dangerouslySetInnerHTML without hydration), and promote the first content image from loading="lazy" to loading="eager" fetchpriority="high" — Ghost lazy-loads every image and the first is usually the LCP. Add suppressHydrationWarning on the <article> because those handlers mutate classes before React hydrates.
  7. next/image remote patterns: derive the Ghost hostname from GHOST_URL at build time in next.config.mjs, plus static.ghost.org and the upload CDN (managed Ghost hosts like DigitalPress serve uploads from **.digitaloceanspaces.com). Set minimumCacheTTL long (e.g. 31 days) — Ghost upload URLs are immutable.
  8. Ghost SEO fields with fallbacks in generateMetadata: meta_title || title, meta_description || excerpt(160), og_image || feature_image, honor canonical_url. Type article + publishedTime / modifiedTime / authors / tags on posts.
  9. Internal blog navigation must use next/link (or next-view-transitions Link) — raw <a> tags (e.g. a navbar "Blog" tab) cause full page reloads.
  10. 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.
  11. Next 15+/16: params is a Promiseconst { slug } = await params; in pages and generateMetadata.

Known operational pitfalls

  • GHOST_CONTENT_API_KEY silently 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_URL at 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-Signature header, not the query param. Pass the shared secret in the target URL (.../api/revalidate?secret=...) and compare against GHOST_REVALIDATE_SECRET.
  • Canonical/OG base URL must not redirect: if the apex 307s to www, some scrapers (e.g. Telegram og:image) choke. Point BASE_URL at 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.