npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ghost-blocks

v0.3.1

Published

Publish to Ghost CMS using a simple, flat content blocks format. Wraps the Ghost Admin API and builds Lexical documents internally.

Readme

ghost-blocks

npm version License

Node.js library for publishing to Ghost CMS using a simple, flat content blocks format. Internally builds Lexical documents and handles JWT authentication.

Install

npm install ghost-blocks

Requires Node.js 18+.

Using with AI agents

If you're generating blog content with an AI agent (Claude, GPT, etc.) and piping it into ghost-blocks, two ready-to-use resources ship with the package:

schema/ai-prompt.md — A copy-paste system prompt template that teaches an AI agent how to produce valid content blocks. Drop it into your agent's system message.

schema/blocks.schema.json — Formal JSON Schema (Draft-7) for strict structured-output validation. Use with OpenAI's response_format or Anthropic's tool_use input_schema.

import { getAiPromptTemplate, getContentBlocksJsonSchema } from 'ghost-blocks';

const systemPrompt = `You are a blog writer. ${getAiPromptTemplate()}`;
const jsonSchema = getContentBlocksJsonSchema();

// Pass `jsonSchema` to OpenAI structured outputs:
//   response_format: { type: 'json_schema', json_schema: { name: 'blogPost', schema: jsonSchema, strict: true } }
// or to Anthropic tool_use:
//   tools: [{ name: 'submit_blog_post', input_schema: jsonSchema }]

Authentication

Create a custom integration in Ghost Admin:

  1. Settings → Integrations → Add custom integration
  2. Copy the Admin API Key (format: {id}:{hex_secret})

The library uses this key to generate short-lived JWT tokens internally on every request.

Usage

Basic post

import { GhostPublisher } from 'ghost-blocks';

const publisher = new GhostPublisher({
  url: 'https://my-site.ghost.io',
  adminKey: process.env.GHOST_ADMIN_KEY!,
});

const post = await publisher.createPost({
  title: 'Hello, world',
  content: [
    { type: 'paragraph', text: 'My first post.' },
  ],
  status: 'published',
});

console.log(post.url);

Rich content

await publisher.createPost({
  title: 'Weekly Market Update',
  status: 'draft',
  excerpt: 'This week in finance.',
  tags: ['Markets', 'Weekly', '#internal-research'],
  feature_image: 'https://example.com/hero.jpg',
  content: [
    { type: 'paragraph', text: 'Opening with **bold** and *italic*.' },
    { type: 'heading', level: 2, text: 'Key Takeaways' },
    { type: 'callout', text: 'S&P 500 up 2.3% this week.', emoji: '📈', color: 'green' },
    { type: 'image', src: 'https://example.com/chart.png', alt: 'Chart' },
    { type: 'divider' },
    { type: 'button', text: 'Read More', url: 'https://example.com', alignment: 'center' },
    { type: 'paywall' },
    { type: 'paragraph', text: 'Premium content here.' },
  ],
  seo: {
    meta_title: 'Weekly Market Update',
    meta_description: 'This week in finance — concise analysis under 160 chars.',
  },
  newsletter: {
    send: true,
    slug: 'weekly-newsletter',
    segment: 'status:free',
  },
});

Update a post

updatePost automatically fetches the current updated_at for collision detection — you don't need to manage it yourself.

await publisher.updatePost(postId, {
  title: 'Updated title',
  content: [{ type: 'paragraph', text: 'Replaced.' }],
});

Note: Tag and author arrays are replaced, not merged. Always send the full desired state.

List posts

const { posts, meta } = await publisher.browsePosts({
  status: 'published',
  tag: 'finance',
  limit: 10,
  order: 'published_at desc',
});

Other operations

const tags = await publisher.listTags();
const newsletters = await publisher.listNewsletters();
await publisher.deletePost(postId);

// Upload an image and use the returned URL in a content block
const url = await publisher.uploadImageFromPath('./hero.jpg');

Content Block Reference

Every block has a type field. All other fields are documented below.

Text blocks

{ type: 'paragraph', text: string }
{ type: 'heading', text: string, level?: 1 | 2 | 3 | 4 | 5 | 6 }   // default level: 2
{ type: 'quote', text: string }

The text field supports inline markdown:

  • **bold**bold
  • *italic*italic
  • ***bold italic***bold italic
  • `code`code
  • [link text](url) → clickable link

Media blocks

{ type: 'image', src: string, alt?: string, caption?: string, width?: number | 'wide' | 'full' }
{ type: 'gallery', images: Array<{ src, alt?, caption?, width?, height? }> }
{ type: 'video', src: string, caption?: string, thumbnail?: string }
{ type: 'audio', src: string, title?: string, duration?: number }
{ type: 'file', src: string, title?: string, filename?: string, caption?: string }
{ type: 'embed', url: string }   // YouTube, Vimeo, Twitter, Spotify, SoundCloud, CodePen — auto-detects via oEmbed

Layout blocks

{ type: 'divider' }
{ type: 'paywall' }
{ type: 'header', heading: string, subheading?: string, background_image?: string, button_text?: string, button_url?: string }
{ type: 'toggle', heading: string, content: string }

Interactive blocks

{ type: 'button', text: string, url: string, alignment?: 'left' | 'center' | 'right' }
{ type: 'callout', text: string, emoji?: string, color?: string }
{ type: 'bookmark', url: string, /* metadata auto-fetched from OpenGraph */ }
{ type: 'signup', heading?, subheading?, button_text?, button_color?, background_color?, ... }
{ type: 'call_to_action', text?, button_text?, button_url? }
{ type: 'product', title: string, description?, image?, button_text?, button_url?, rating? }

Code/raw

{ type: 'codeblock', code: string, language?: string, caption?: string }
{ type: 'html', html: string }
{ type: 'markdown', markdown: string }

Email-only blocks

{ type: 'email_content', html: string }   // Visible in email only, not on web
{ type: 'email_cta', text?: string, button_text?: string, button_url?: string, segment?: string }

Advanced

Skip enrichment for offline/test scenarios

By default, bookmark blocks fetch OpenGraph metadata and embed blocks fetch oEmbed data. Disable for tests:

import { GhostPublisher, LexicalBuilder } from 'ghost-blocks';

const publisher = new GhostPublisher({ url, adminKey });
publisher.builder = new LexicalBuilder({ skipEnrichment: true });

Build Lexical without publishing

const lexical = await publisher.buildLexical([
  { type: 'paragraph', text: 'preview' },
]);
// lexical is a JSON string ready for Ghost's `lexical` field

Direct API access

For advanced use cases, the underlying GhostClient is exposed:

publisher.client.createPost({ title: 'Raw', lexical: '...' });
publisher.client.uploadImageBuffer(buffer, 'photo.jpg', 'image/jpeg');

Security

This library was built with security in mind:

  • Timing-safe authentication — JWT secrets compared with crypto.timingSafeEqual

  • SSRF protectionbookmark and embed URLs are validated against private IP ranges and Docker hostnames before any HTTP request. Customize via UrlValidator:

    import { UrlValidator, OpenGraphFetcher, LexicalBuilder } from 'ghost-blocks';
    
    const validator = new UrlValidator({
      extraBlockedHostnames: ['my-internal-service'],
    });
    const opengraph = new OpenGraphFetcher({ validator });
    const builder = new LexicalBuilder({ opengraph });
  • No code injection fieldscodeinjection_head/codeinjection_foot are intentionally not exposed (these inject arbitrary JS into every page).

TypeScript

Full TypeScript types ship with the package. The main types you'll use:

import type {
  GhostPublisher,
  ContentBlock,
  CreatePostInput,
  UpdatePostInput,
  PostSummary,
  PostStatus,
  PostVisibility,
  Tag,
  Newsletter,
} from 'ghost-blocks';

More from the author

If you publish to Ghost from n8n workflows, you might also like:

  • n8n-nodes-ghost-blocks — The official n8n community node wrapping this library. Drag-and-drop publishing in n8n.
  • Nodey (launching soon) — A mobile command centre for n8n. Run and debug workflows from your phone, with an AI workflow builder, geo-fenced location-based triggers, and NFC triggers.
  • n8n workflow templates — A growing collection of production-ready n8n workflow templates.

License

MIT