Skip to content

Repository files navigation

themeseed

Fill a CMS with realistic demo content so you can judge a theme against real-looking articles — instead of an empty install, a "Coming soon" placeholder, or a wall of lorem ipsum.

themeseed reads the active theme first, works out what it can actually display, and then generates content that fits: hero images at the theme's own aspect ratio, galleries only where the theme has gallery styles, real embedded videos only where it has embed styles, and articles long enough to fill the layout the designer built.

It ships as both an MCP server (drive it from Claude Code, Cursor, Windsurf, Claude Desktop, VS Code, Zed…) and a CLI.

themeseed add-site --slug blog --url http://localhost:2368
themeseed analyze blog          # what can this theme display?
themeseed seed blog -t "SaaS productivity blog" -c 15
themeseed wipe blog             # remove exactly what it created

Contents


Why

Evaluating a theme against an empty site tells you almost nothing. The hero looks fine because there is no hero. The archive grid looks tidy because there are three posts. The typography looks great for 200 words and falls apart at 1,500.

Filling the site by hand is an hour of work you throw away. Filling it with lorem ipsum tells you about the typography and nothing about the layout, because lorem ipsum has no headings, no images, no galleries, no pull quotes and no embeds.

themeseed produces content shaped like the real thing — and shaped like this theme's real thing, because it reads the theme before it writes anything.

Install

themeseed is published to IndiaNIC's private registry. Point the @indianic scope at it once:

npm config set @indianic:registry https://npm.indianic.in/
npm login --registry https://npm.indianic.in/ --scope @indianic

Then install globally:

npm install -g @indianic/themeseed

Or run it without installing:

npx @indianic/themeseed init

Node 20 or newer is required.

Registry note. The public-npm / public-GitHub story is still undecided — see Open questions. Today this package exists only on npm.indianic.in.

Quick start

themeseed init

init does three things:

  1. Detects MCP-capable tools installed on your machine.
  2. Asks which of them to register the themeseed MCP server with, and writes each one's config in its own native format (merging, never replacing).
  3. Offers to add your first site.

Adding a site verifies the credentials before saving them, so a bad key fails immediately rather than half way through a seed run.

$ themeseed analyze blog

bohomian v1.0.0
Bohomian — a quiet, Scandinavian Ghost theme.

  feature image   yes (ratio ≈ 1.33)
  gallery card    yes
  video embed     yes
  bookmark card   yes
  wide images     yes
  shows tags      yes
  shows author    yes (with avatar)
  reading time    yes
  target length   ~1400 words (910–2030)
  posts per page  12

  confidence 0.95 via local-theme-files, rendered-site

Then fill it:

$ themeseed seed blog -t "SaaS productivity blog" -c 15

✔ Created 15 of 15 post(s) on "blog"

  theme            bohomian
  feature images   15
  inline images    5
  galleries        5
  video embeds     5

Every post is tagged #themeseed (an internal, non-public tag on Ghost), which is how themeseed wipe removes exactly this content and nothing else.

CLI reference

themeseed uses commander for argument parsing and @clack/prompts for interactive input. Prompts degrade to plain line output when stdout is not a TTY, so piping and CI logs stay readable.

Setup

Command What it does
themeseed init First-run setup: write config files, register the MCP server, pick image providers, add a site
themeseed images [--list] Configure where post images come from, at any time
themeseed install [-t <id>…] [-a] Register the MCP server with an editor, without the full init flow
themeseed uninstall [-t <id>…] Remove the MCP server from editor configuration
themeseed mcp-config Print the JSON snippet for manual configuration
themeseed upgrade [--check] Check the registry for a newer version and install it
themeseed --version / -v Print the installed version
themeseed --help / -h Show help (also themeseed help <command>)

Sites

Command What it does
themeseed add-site Register a CMS. Verifies credentials before saving
themeseed remove-site [slug] Forget a site. Deletes no content
themeseed list-sites List configured sites (never prints credentials)
themeseed use <slug> Set the default site used when no slug is given

add-site accepts --slug, --platform, --url, --key and --themes-dir for scripting. Prefer the interactive prompt for the key: a credential passed as a flag lands in your shell history and in ps output.

Content

Command What it does
themeseed analyze [site] [--json] Report what the active theme can display, with evidence
themeseed seed [site] Generate and publish demo content
themeseed list [site] [--json] List content themeseed created
themeseed wipe [site] [-y] Delete everything themeseed created

seed options:

Flag Default Meaning
-t, --topic <topic> prompted What the publication is about
-c, --count <n> 12 How many posts to create
-i, --image-source <source> auto auto, local, stock, ai or none
--study-theme off Read the active theme and shape content to it
--draft off Create drafts instead of published posts
--author <name> Author to attribute posts to
--no-video off Skip YouTube lookups (faster, fully offline)
--seed <n> topic hash Seed the generator for reproducible output
-y, --yes off Skip the confirmation prompt

MCP tools

Run themeseed install to register the server, or add it by hand:

{
  "mcpServers": {
    "themeseed": {
      "command": "npx",
      "args": ["-y", "--package", "@indianic/themeseed", "themeseed-mcp"],
      "env": { "npm_config_registry": "https://npm.indianic.in/" }
    }
  }
}

VS Code nests servers under servers rather than mcpServers; Zed calls them context_servers. themeseed install handles each format for you.

Tool Parameters What it does
analyze_theme site? Reports theme capabilities, with evidence and a confidence score
generate_posts site?, topic, count, imageSource, status, titles?, authorName?, includeVideo Analyzes, generates, sources images, publishes
list_seeded site? Lists everything themeseed created
wipe_seeded site?, confirm Deletes it. Defaults to a dry run — confirm: true to proceed
add_site slug, platform, url, credentials, themesDir? Registers a site, verifying credentials first
list_sites Lists sites. Never returns credentials
remove_site slug Forgets a site

Tip: pass your own headlines to generate_posts via titles. The built-in engine writes serviceable copy, but a model that knows the client's voice writes better copy — this lets it supply the words while themeseed handles structure, images, capability-gating and publishing.

How theme analysis works

This is the part that makes the output fit rather than merely exist. It is opt-in, because it costs a round trip and only pays for itself when you want content shaped to a specific theme:

themeseed seed blog --topic "SaaS productivity" --study-theme
themeseed analyze blog          # read the theme without publishing anything

Over MCP, set studyTheme: true on generate_posts. Without it, themeseed generates against neutral assumptions — a feature image, tags, an author, about 850 words, and no cards a theme might not style.

Ghost's Admin API will not serve theme files: GET /ghost/api/admin/themes/ returns 403 NoPermissionError for API-token auth on every Ghost version, because it requires a staff session. So themeseed uses two strategies and merges them.

1. Local theme files (confidence 0.9) — when Ghost runs on this machine and you point --themes-dir at content/themes. Reads the active theme's .hbs templates, package.json and compiled CSS. This is as close to ground truth as it gets: card_assets: true proves every Koenig card is styled; {{#if feature_image}} proves the hero is rendered; a table-of-contents partial proves the theme expects long articles.

2. The rendered site (confidence 0.6) — always available, including Ghost(Pro) and any remote install. The trick: a theme's own stylesheet is a public asset. Fetching it and looking for kg-gallery-container, kg-embed-card, kg-width-wide answers the card questions with real evidence rather than assumption. The rendered post page supplies the rest — hero, tags, author avatar, reading time.

The higher-confidence strategy wins any contested field; the lower one fills what nobody else measured. Anything still unmeasured falls back to conservative defaults and lowers the reported confidence, so a low number means "we guessed", not "the theme is limited".

Which theme is active comes from GET /ghost/api/admin/settings/ (active_theme), which API tokens may read.

Every conclusion is recorded in evidence[]:

- [local-theme-files] package.json config.card_assets enables Ghost card styles for all cards
- [local-theme-files] feature image aspect-ratio ≈ 1.33 (most common of the ratios attached to
  feature images; 3 occurrence(s), via class)
- [rendered-site] card selectors in served CSS — gallery:true embed:true bookmark:true wide:true

Why capability-gating matters

A gallery card in a theme with no gallery styles renders as a broken-looking stack of images. That is worse than the plain post it replaced, because it makes a good theme look buggy. themeseed only emits a block when the theme was measured to support it.

Image sources

One interface, four interchangeable implementations, selected with --image-source.

Images are optional. With no provider key configured, posts publish as text, which is a fair preview of a theme in its own right. Configure a provider whenever you want one:

themeseed images                 # interactive
themeseed images --list          # what is configured, and what `auto` picks
themeseed images --set unsplash --key <key>
themeseed images --set local --key /Users/you/Pictures/seed
themeseed images --remove unsplash

Keys are stored in ~/.themeseed/.env. themeseed init asks about this too.

auto (the default)

Uses ai when an AI key is set, stock when a stock key is, and none otherwise. A generated image matches the post's subject exactly; a searched photograph is at least topical; neither is worth faking with unrelated placeholder art. auto never fails — it degrades. Naming a source explicitly is a decision rather than a preference, so stock and ai still fail loudly when their key is missing.

none

Sources no images. Posts publish as text.

local

Scans a directory (THEMESEED_LOCAL_IMAGE_DIR) and picks files whose aspect ratio is closest to what the theme wants. Fully offline and reproducible. Descriptive filenames become alt text; camera filenames like IMG_4821 are rejected in favour of the query, because bad alt text is worse than generic alt text.

stock

Adapter Key Query-relevant?
unsplash UNSPLASH_ACCESS_KEY yes
pexels PEXELS_API_KEY yes
picsum none no — real photos, but random

Picks whichever key is present, else falls back to Lorem Picsum so the zero-config path still works. Picsum returns genuine photographs at the right dimensions, which is enough to judge a theme's layout, and useless for judging whether images match the copy — so every image it returns carries a credit saying exactly that. Set THEMESEED_STOCK_PROVIDER to force one.

ai

Adapter Key Default model Actually AI?
openai OPENAI_API_KEY gpt-image-1 yes
grok XAI_API_KEY grok-imagine-image yes
gemini GOOGLE_API_KEY gemini-2.5-flash-image yes
fal FAL_KEY fal-ai/flux/schnell yes
procedural none no

Whichever key is present is used; THEMESEED_AI_IMAGE_ADAPTER forces one and THEMESEED_AI_IMAGE_MODEL overrides the model. Each backend asks for output shape differently — aspect_ratio, imageConfig.aspectRatio, named size presets — so themeseed snaps the theme's measured ratio onto whatever that backend supports and the generator only ever asks for a number.

Two things worth knowing before picking one:

  • Gemini image models require billing enabled on the project. Without it every call returns 429 RESOURCE_EXHAUSTED immediately, which reads like a rate limit that will clear on its own. It will not.
  • fal keys are <id>:<secret>, sent as Authorization: Key … rather than Bearer.

procedural renders deterministic abstract artwork locally — gradients and soft geometry, no network, no key. It exists so --image-source ai still produces valid, correctly-sized images out of the box and so the integration tests are hermetic. It is not AI, and every image it produces says so in its credit line.

Check what actually works on your machine, one image per configured adapter:

npx tsx scripts/probe-ai-adapters.ts --ratio 1.5

Adding a backend (Replicate, Stability, a local diffusion server) means implementing AiImageAdapter and adding one line to selectAiAdapter — see src/images/ai-source.ts.

Validation

Every image is parsed before upload. Stock APIs and CDNs return HTML error pages with image URLs often enough that uploading unverified bytes reliably produces posts full of broken images, so bytes that are not a decodable PNG/JPEG/GIF/WebP/AVIF are dropped with a warning.

Content generation

The default engine is deterministic, offline and free. It composes each paragraph from a claim, an elaboration and usually an example or caveat, drawn from rotating decks so a 1,400-word article does not visibly repeat. Titles cycle through five article shapes — guide, listicle, opinion, case study, explainer — so a batch of fifteen looks like a real publication's archive rather than fifteen variations of one headline.

It is not trying to write good essays. It is trying to produce headings, paragraph rhythm, article length, pull quotes, lists and images that stress a layout the way real content will.

Two ways to get better prose without touching the code:

  • Pass titles (MCP) or use your own copy — the host model writes, themeseed structures.
  • Implement ContentEngine (src/content/engine.ts) and pass it to generateSeedContent.

Videos are always real. themeseed searches YouTube, then confirms each candidate through the public oEmbed endpoint — which only answers for videos that exist and permit embedding — and takes the title, author and thumbnail from that response. A fabricated video ID renders as "Video unavailable", which looks like a broken theme, so ids are never invented.

Supported platforms

Platform Status
Ghost ✅ Supported — Admin API, Lexical, image upload, theme analysis
WordPress 📋 Planned — contributions welcome
Joomla 📋 Planned — contributions welcome
Drupal 📋 Planned — contributions welcome
Magento 📋 Planned — contributions welcome

Adding one is a self-contained job: implement CmsProvider, register it, add tests. Nothing in the content generator, image sources, CLI or MCP tools needs to change. See CONTRIBUTING.md.

Configuration

Everything lives in ~/.themeseed/, never inside a repository:

File What it holds
sites.json The sites you seed, with credentials. Written by themeseed add-site
.env Image and AI provider keys. Written by themeseed init and images
sites.example.json A documented template for sites.json, with every field explained

themeseed init creates all three. Every setting appears in .env already, commented out, so you can fill it in with an editor instead of a prompt — themeseed never overwrites that file. Set THEMESEED_CONFIG_DIR to move the directory elsewhere.

Both the CLI and the MCP server read ~/.themeseed/.env by absolute path. They deliberately do not read a .env from the current directory: an editor launches the MCP server in whatever directory it has open, so a relative lookup finds a different file every time, or none.

sites.json is written 0600:

{
  "version": 1,
  "defaultSite": "blog",
  "sites": {
    "blog": {
      "platform": "ghost",
      "url": "http://localhost:2368",
      "credentials": { "adminApiKey": "<id>:<secret>" },
      "options": { "themesDir": "/path/to/ghost/content/themes" }
    }
  }
}

Get a Ghost Admin API key from Ghost Admin → Settings → Integrations → Add custom integration. It is the key with a colon in it — the Content API key will not work.

Every environment variable is listed and documented in the generated ~/.themeseed/.env. (The repository's own .env.example configures the e2e suite, not normal use.) THEMESEED_LOG_LEVEL=debug turns on verbose diagnostics; all logging goes to stderr, because stdout belongs to the MCP protocol.

Architecture

src/
  core/types.ts            platform-neutral domain model — the contract everything shares
  core/seed.ts             analyze → generate → publish, used by both CLI and MCP
  providers/provider.ts    the CmsProvider interface
  providers/registry.ts    platform name → implementation (one line per platform)
  providers/ghost/         the only place Ghost's vocabulary exists
  content/generator.ts     topic + capabilities → neutral blocks
  content/engine.ts        prose, behind a swappable interface
  images/                  local | stock | ai, behind one ImageSource interface
  mcp/                     MCP server and tools
  cli/                     commander + clack

Two rules hold the whole thing together:

  1. Nothing outside src/providers/<platform>/ may import that platform's types. Shared code talks only to CmsProvider.
  2. The generator emits neutral blocks, never a CMS storage format. Conversion to Ghost's Lexical happens only in src/providers/ghost/lexical.ts. This is what makes WordPress support a provider-only change rather than a rewrite.

Development

npm install
cp .env.example .env      # point GHOST_TEST_BLOG_* at a disposable Ghost
npm run verify            # typecheck + lint + unit tests
npm run test:e2e          # integration tests against the real instance
npm run test:loop         # the autonomous seed/verify/wipe loop

npm run test:loop seeds 15 posts, verifies them by reading back through Ghost's own Admin API (deliberately not through themeseed's listSeeded, so a bug affecting both cannot hide), and repeats until three consecutive clean runs, then wipes and confirms zero. Each iteration is appended to test-loop.log.

Handy probes, none of which are part of the test suite:

npx tsx scripts/probe-theme.ts --themes-dir /path/to/content/themes
npx tsx scripts/probe-images.ts
npx tsx scripts/probe-video.ts "deep work"
npm run build && npx tsx scripts/probe-mcp.ts

Open questions

Three decisions are deliberately not settled in code. See OPEN-QUESTIONS.md for the full trade-offs.

  1. License — currently MIT, provisionally. Confirm MIT vs Apache-2.0.
  2. Distribution — private to npm.indianic.in, or also public npm/GitHub under a public org.
  3. Stock image provider — standardise on Unsplash, Pexels, both, or keep all three.

License

MIT — see LICENSE. Provisional pending the decision above.

Validated

M8ven Score

About

Seed realistic demo content into any CMS so you can preview a theme with real articles, images, galleries and video. MCP server + CLI. Ghost today; WordPress, Joomla, Drupal and Magento planned.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages