Skip to content

magicpages/ghost-typesense

Repository files navigation

@magicpages/ghost-typesense

Add powerful search to your Ghost blog with Typesense. This monorepo provides everything you need:

  • πŸ” Search UI: Beautiful, accessible search interface
  • πŸ€– CLI Tool: Easy content syncing and management
  • πŸͺ Webhook Handler: Real-time content updates

Quick Start

1. Set Up Typesense

You'll need:

  • A Typesense instance (cloud or self-hosted)
  • Admin API key (for syncing content)
  • Search-only API key (for the search UI)

2. Add Search to Your Theme

There are two ways to add search to your Ghost site:

Option 1: Replace Ghost's Default Search (Recommended)

Add to your config.[environment].json:

"sodoSearch": {
    "url": "https://unpkg.com/@magicpages/ghost-typesense-search-ui/dist/search.min.js"
}

Or set the environment variable:

sodoSearch__url=https://unpkg.com/@magicpages/ghost-typesense-search-ui/dist/search.min.js

Option 2: Code Injection

If you're using a managed host like Ghost(Pro), add this to your site's code injection (Settings β†’ Code injection β†’ Site Header):

<script src="https://unpkg.com/@magicpages/ghost-typesense-search-ui/dist/search.min.js"></script>

You can also self-host the search.min.js and add that URL instead of https://unpkg.com/@magicpages/ghost-typesense-search-ui/dist/search.min.js.

Self-hosting note: the default modal layout needs only search.min.js. If you opt into the palette or discovery layout (see below), the widget lazily loads palette.min.js / discovery.min.js from the same directory as search.min.js, so deploy those chunks alongside it. unpkg and the npm package already include them.

For either of these options, you'll then need to add a code injection into your site's header to configure the search UI:

<script>
  window.__MP_SEARCH_CONFIG__ = {
    typesenseNodes: [{
      host: 'your-typesense-host',
      port: '443',
      protocol: 'https'
    }],
    typesenseApiKey: 'your-search-only-api-key',
    collectionName: 'ghost',
    theme: 'system'  // Optional: 'light', 'dark', or 'system'
  };
</script>

Choosing a UI layout

The widget ships three interchangeable layouts, selected with uiStyle:

  • 'modal' (default) β€” a centered modal with rich result rows; supports a list or grid template.
  • 'palette' β€” a keyboard-first command palette (⌘K idiom) with grouped results and recent searches.
  • 'discovery' β€” a two-pane content explorer with a live preview and a facet rail.
<script>
  window.__MP_SEARCH_CONFIG__ = {
    // ... required config
    uiStyle: 'discovery'  // 'modal' (default) | 'palette' | 'discovery'
  };
</script>

The install line is identical for every layout β€” one <script> tag. Only the layout you choose is downloaded by the reader. See the search-ui README for the full layout, keyboard, theming, facet, and i18n reference.

3. Initial Content Sync

  1. Install the CLI:
npm install -g @magicpages/ghost-typesense-cli
  1. Create ghost-typesense.config.json:
{
  "ghost": {
    "url": "https://your-ghost-blog.com",
    "key": "your-content-api-key",
    "version": "v5.0"
  },
  "typesense": {
    "nodes": [{
      "host": "your-typesense-host",
      "port": 443,
      "protocol": "https"
    }],
    "apiKey": "your-admin-api-key"
  },
  "collection": {
    "name": "ghost"
  }
}
  1. Initialize and sync:
ghost-typesense init --config ghost-typesense.config.json
ghost-typesense sync --config ghost-typesense.config.json

4. Set Up Real-Time Updates (Optional)

To keep your search index in sync with your content:

  1. Deploy the webhook handler to Netlify:

Deploy to Netlify

  1. Set these environment variables in Netlify (Site settings β†’ Environment variables):
GHOST_URL=https://your-ghost-blog.com
GHOST_CONTENT_API_KEY=your-content-api-key  # From Ghost Admin
TYPESENSE_HOST=your-typesense-host
TYPESENSE_API_KEY=your-admin-api-key  # Typesense Admin API key
COLLECTION_NAME=ghost  # Must match search config
WEBHOOK_SECRET=your-secret-key  # Generate a random string
  1. Set up webhooks in Ghost Admin:
    • Go to Settings β†’ Integrations
    • Create/select a Custom Integration
    • Give it a name (e.g. "Typesense Search")
    • Add these webhooks:
Event Target URL
Post published https://your-site.netlify.app/.netlify/functions/handler?secret=your-secret-key
Post updated https://your-site.netlify.app/.netlify/functions/handler?secret=your-secret-key
Post deleted https://your-site.netlify.app/.netlify/functions/handler?secret=your-secret-key
Post unpublished https://your-site.netlify.app/.netlify/functions/handler?secret=your-secret-key

Now your search index will automatically update when you publish, update, or delete posts!

Semantic search

By default search is purely lexical. You can optionally enable semantic (hybrid) search, where Typesense ranks results by a fusion of keyword relevance and vector similarity β€” so a query matches on meaning, not just shared words. This needs no extra infrastructure: Typesense generates the embeddings itself.

1. Add an embedding field to the collection schema

Add a float[] field with an embed block to your collection.fields in ghost-typesense.config.json. The from fields are the content Typesense embeds; model_config.model_name selects the model.

{
  "collection": {
    "name": "ghost",
    "fields": [
      { "name": "embedding", "type": "float[]", "optional": true,
        "embed": {
          "from": ["title", "plaintext", "excerpt"],
          "model_config": { "model_name": "ts/all-MiniLM-L12-v2" }
        }
      }
    ]
  }
}

When you provide a custom fields array, the required content fields (id, title, url, slug, html, plaintext, excerpt, published_at, updated_at) are still enforced and merged in automatically, so you only need to add the embedding field itself.

Models. You can use a built-in Typesense model (e.g. ts/all-MiniLM-L12-v2) which runs locally on the Typesense server at no per-document cost, or an external provider by passing its details in model_config (for example an OpenAI model with model_name, api_key). Built-in models keep everything self-contained; external models can offer higher quality at a per-document API cost.

Then init and sync as usual β€” Typesense embeds each document at index time:

ghost-typesense init --config ghost-typesense.config.json
ghost-typesense sync --config ghost-typesense.config.json

2. Enable hybrid querying in the search UI

Set semanticSearch: true in your search config β€” see the search-ui semantic search docs. By default the widget biases hybrid results toward keyword matches (semanticAlpha: 0.2) and drops distant vector-only matches (semanticDistanceThreshold: 0.8), both tunable β€” see Keeping hybrid results relevant.

To make author names matchable as a keyword query (e.g. searching a contributor's name), set searchAuthors: true β€” see Searchable fields.

Requirements and tradeoffs

  • Typesense version. Auto-embedding with built-in models requires Typesense v0.25.0 or newer (the build that ships the ML models). External-provider embedding is available from the same versions.
  • Memory. Vector fields meaningfully increase a collection's RAM footprint. Benchmark on a representative slice of your content before enabling it for a large blog.
  • Index time. Generating embeddings adds latency to syncing. Built-in models add CPU time on the Typesense server; external providers add per-document API calls (and their cost). Large initial syncs take noticeably longer than lexical-only indexing.

Members-only content

By default, only public published posts are indexed. Members-only and paid posts are skipped entirely, so a blog that publishes mostly gated content will have a near-empty search index.

You can opt in to indexing gated posts as redacted documents β€” discoverable in search, but without exposing the protected body. Set indexGatedContent on the collection config:

{
  "collection": {
    "name": "ghost",
    "indexGatedContent": true
  }
}

With it enabled:

  • Non-public posts (members, paid, tier-restricted) are indexed with their title, excerpt, URL, tags, and feature image, plus a visibility field.
  • The searchable text is limited to the public excerpt (falling back to the title). The post's body is never read or indexed β€” this package uses Ghost's Content API, which only ever returns the public preview for gated posts, and the indexer ignores the body regardless. There is no protected text in the index to leak.
  • The search UI marks these results with a "members only" badge (see the search-ui README), turning gated posts into discoverable lead magnets.

For real-time updates, set INDEX_GATED_CONTENT=true on the webhook handler to mirror this behaviour.

Packages

Package Description
@magicpages/ghost-typesense-search-ui Search interface that matches your Ghost theme
@magicpages/ghost-typesense-cli CLI tool for content syncing
@magicpages/ghost-typesense-webhook Webhook handler for real-time updates

License

MIT Β© MagicPages

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors