agentsclimarketplace

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

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.

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-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.

What ships with it: 10 files

54.0 KB alongside SKILL.md, 3 of them executable

references/

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.

Keep looking

Skills are one crate of 325,949. 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.