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

@magicpages/ghost-typesense-search-ui

v1.12.0

Published

A vanilla JavaScript search UI for Ghost + Typesense integration

Downloads

312

Readme

@magicpages/ghost-typesense-search-ui

A beautiful, accessible search interface for Ghost blogs using Typesense. This package provides a drop-in replacement for Ghost's default search functionality, offering enhanced features and seamless integration with Typesense.

Search UI Preview

Features

  • 🔍 Real-time search powered by Typesense (needs a Typesense server)
  • 🎨 Beautiful, accessible interface
  • 🌓 Automatic dark mode support
  • ⌨️ Full keyboard navigation
  • 📱 Responsive design for all devices
  • 🎯 Configurable common searches suggestions
  • ⚡ Lightweight and performant
  • 🔎 Smart context-aware search result highlighting
  • 📝 Plaintext content search for improved relevance
  • 💡 Exact phrase matching support
  • 🔍 Contextual excerpts that show search term in context
  • 🧩 Support for nested fields
  • 🛡️ Style encapsulation with Web Components - Uses Shadow DOM to prevent Ghost theme styles from interfering with the search UI

Installation

There are two ways to integrate the search UI into your Ghost site:

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

This is the preferred method as it prevents loading two search scripts, resulting in better performance. You'll need access to your Ghost configuration.

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 Method

If you're using a managed Ghost host like Ghost(Pro) where you can't modify the configuration, use this method. The script will automatically remove any traces of the default search to prevent conflicts, but cannot prevent the sodo-search.min.js from being loaded.

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

For either method, 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.

Configuration

Configure the search by adding a global configuration object before loading the script. You can add this to your theme, or use Ghost's code injection to add it to your site's header.

<script>
window.__MP_SEARCH_CONFIG__ = {
    typesenseNodes: [{
        host: 'your-typesense-host',
        port: '443',
        protocol: 'https'
    }], // also supports a Typesense cluster
    typesenseApiKey: 'your-search-only-api-key', // Under no circumstances use an admin API key here. These values are stored client-side and are therefore accessible to the end user.
    collectionName: 'your-collection-name',
    theme: 'system', // 'light', 'dark', or 'system'
    enableHighlighting: true, // highlight search terms in results
    commonSearches: ['Getting Started', 'Tutorials', 'API'] // can also be empty
};
</script>

Configuration Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | typesenseNodes | Array | Yes | — | Array of Typesense node configurations (host, port, protocol) | | typesenseApiKey | String | Yes | — | Search-only API key from Typesense | | collectionName | String | Yes | — | Name of your Typesense collection | | theme | String | No | 'system' | UI theme: 'light', 'dark', or 'system' (respects OS preference) | | enableHighlighting | Boolean | No | true | Whether to highlight search terms in results | | commonSearches | Array | No | [] | Array of suggested search terms to display | | searchFields | Object | No | See below | Customize field weights and highlighting | | typesenseSearchParams | Object | No | {} | Override default Typesense search parameters (sorting, filtering, etc.) | | transformToRelativeUrls | Boolean | No | false | Convert result URLs to relative paths (useful for proxy domains or custom domain setups) | | locale | String | No | 'en' | Locale identifier for i18n translations | | i18n | Object | No | {} | Translation overrides for UI strings (see Internationalization) |

Search Fields Configuration

Customize search relevance by assigning weights and enabling highlighting per field. Higher weights mean matches in that field are considered more relevant.

Default configuration:

searchFields: {
    title: { weight: 5, highlight: true },
    plaintext: { weight: 4, highlight: true },
    'tags.name': { weight: 4, highlight: true },
    excerpt: { weight: 3, highlight: true },
    'tags.slug': { weight: 3, highlight: true }
}

| Field | Default Weight | Description | |-------|---------------|-------------| | title | 5 | Post/page title — highest relevance | | plaintext | 4 | Plain text content of the post | | tags.name | 4 | Tag display names | | excerpt | 3 | Post excerpt | | tags.slug | 3 | Tag URL slugs |

You can add additional fields (e.g., html) or adjust weights to tune relevance for your content:

searchFields: {
    title: { weight: 10, highlight: true },    // Boost title matches even more
    plaintext: { weight: 5, highlight: true },
    excerpt: { weight: 3, highlight: true },
    html: { weight: 1, highlight: true }        // Include HTML content with low weight
}

Advanced Search Parameters

Use typesenseSearchParams to override any of the default Typesense search parameters. Custom parameters are merged with the defaults, so you only need to specify what you want to change.

Default search parameters:

| Parameter | Default | Description | |-----------|---------|-------------| | query_by | Auto-generated from searchFields | Comma-separated list of fields to search | | query_by_weights | Auto-generated from searchFields | Relevance weights matching query_by fields | | sort_by | '_text_match:desc,published_at:desc' | Sort by text match relevance, then by publication date | | prefix | true | Enable prefix matching (matches partial words as you type) | | per_page | 20 | Number of results per page | | typo_tolerance | false | Whether to allow typo corrections in search | | num_typos | 0 | Maximum number of typos to tolerate per word | | prioritize_exact_match | true | Prioritize results with exact phrase matches | | drop_tokens_threshold | 0 | Token drop threshold for relaxing multi-word queries | | enable_nested_fields | true | Enable searching in nested fields (e.g., tags.name) | | highlight_affix_num_tokens | 30 | Number of surrounding tokens shown in highlighted excerpts | | include_fields | 'title,url,excerpt,plaintext,published_at,tags' | Fields returned in search results |

Sorting examples:

typesenseSearchParams: {
    // Sort by newest first, ignoring text relevance
    sort_by: 'published_at:desc',

    // Sort by relevance only (ignore publication date)
    sort_by: '_text_match:desc',

    // Default: relevance first, then newest
    sort_by: '_text_match:desc,published_at:desc'
}

Enabling typo tolerance:

typesenseSearchParams: {
    typo_tolerance: true,
    num_typos: 2  // Allow up to 2 typos per word
}

Filtering results:

typesenseSearchParams: {
    filter_by: 'tags.slug:=tutorials'  // Only show posts tagged "tutorials"
}

Full override example:

window.__MP_SEARCH_CONFIG__ = {
    // ... required config
    typesenseSearchParams: {
        sort_by: 'published_at:desc',
        per_page: 10,
        typo_tolerance: true,
        num_typos: 1,
        filter_by: 'tags.slug:!=internal'
    }
};

Note: If you provide a custom query_by without a matching query_by_weights, the default weights are automatically removed to avoid mismatches. If you override query_by, you should also provide query_by_weights.

Usage

The search interface can be triggered in multiple ways:

  • Click the search icon in your Ghost theme
  • Press / on your keyboard
  • Navigate to /#/search URL
  • Use URL query parameters: /?s=your search term or /?q=your search term
  • Programmatically via window.magicPagesSearch.openModal()

URL-Based Search

You can trigger searches directly from URLs using two formats:

1. Query Parameter Format

https://yourblog.com/?s=getting+started
https://yourblog.com/?q=tutorials

Both s and q query parameters are supported for maximum compatibility with legacy links.

2. Clean Hash Path Format

https://yourblog.com/#/search/getting+started

Keyboard Shortcuts

  • /: Open search (when not focused on an input field)
  • Cmd/Ctrl + K: Open search
  • ↑/↓: Navigate through results
  • Enter: Select result
  • Esc: Close search

Customization

The search UI automatically detects and uses your Ghost site's accent color by reading the --ghost-accent-color CSS variable. This ensures that the search interface matches your site's branding.

The UI also includes a built-in dark mode that automatically activates based on the user's system preferences. It can also be overwritten in the UI's configuration.

Web Component Architecture

This search UI is built as a Web Component (<magicpages-search>) with Shadow DOM encapsulation. This means:

  • Complete style isolation: Ghost theme styles cannot interfere with the search UI
  • No CSS conflicts: The search UI styles won't leak into your page
  • Consistent appearance: The search UI looks the same regardless of theme styles
  • Better performance: Scoped styles are more efficient

The component still respects your site's accent color through CSS custom properties, which inherit into Shadow DOM.

Internationalization (i18n)

The search UI supports full internationalization, allowing you to translate all UI elements to any language.

Basic Usage

Add translations to your configuration. You only need to provide the strings you want to override:

window.__MP_SEARCH_CONFIG__ = {
  // ... existing config
  locale: 'de', // Optional: specify the locale
  i18n: {
    searchPlaceholder: 'Suche nach allem',
    loadingMessage: 'Suche läuft...',
    noResultsMessage: 'Keine Ergebnisse gefunden'
  }
}

Available Translation Keys

| Key | Default (English) | Description | |-----|-------------------|-------------| | searchPlaceholder | "Search for anything" | Placeholder text in search input | | commonSearchesTitle | "Common searches" | Heading for common searches section | | emptyStateMessage | "Start typing to search..." | Message shown when search is empty | | loadingMessage | "Searching..." | Message shown while searching | | noResultsMessage | "No results found for your search" | Message when no results found | | navigateHint | "to navigate" | Keyboard hint for navigation | | closeHint | "to close" | Keyboard hint for closing | | ariaSearchLabel | "Search" | ARIA label for search input | | ariaCloseLabel | "Close search" | ARIA label for close button | | ariaResultsLabel | "Search results" | ARIA label for results region | | ariaArticleExcerpt | "Article excerpt" | ARIA label for article excerpts | | ariaModalLabel | "Search" | ARIA label for modal | | untitledPost | "Untitled" | Fallback for posts without titles |

Example Translations

German (Deutsch):

i18n: {
  searchPlaceholder: 'Suche nach allem',
  commonSearchesTitle: 'Häufige Suchanfragen',
  emptyStateMessage: 'Beginnen Sie mit der Eingabe...',
  loadingMessage: 'Suche läuft...',
  noResultsMessage: 'Keine Ergebnisse gefunden',
  navigateHint: 'zum Navigieren',
  closeHint: 'zum Schließen',
  ariaSearchLabel: 'Suche',
  ariaCloseLabel: 'Suche schließen',
  untitledPost: 'Ohne Titel'
}

Spanish (Español):

i18n: {
  searchPlaceholder: 'Buscar cualquier cosa',
  commonSearchesTitle: 'Búsquedas comunes',
  emptyStateMessage: 'Comienza a escribir para buscar...',
  loadingMessage: 'Buscando...',
  noResultsMessage: 'No se encontraron resultados',
  navigateHint: 'para navegar',
  closeHint: 'para cerrar',
  ariaSearchLabel: 'Buscar',
  ariaCloseLabel: 'Cerrar búsqueda',
  untitledPost: 'Sin título'
}

French (Français):

i18n: {
  searchPlaceholder: 'Rechercher n\'importe quoi',
  commonSearchesTitle: 'Recherches courantes',
  emptyStateMessage: 'Commencez à taper pour rechercher...',
  loadingMessage: 'Recherche en cours...',
  noResultsMessage: 'Aucun résultat trouvé',
  navigateHint: 'pour naviguer',
  closeHint: 'pour fermer',
  ariaSearchLabel: 'Rechercher',
  ariaCloseLabel: 'Fermer la recherche',
  untitledPost: 'Sans titre'
}

Partial Overrides

You don't need to provide all translation keys. Any keys you don't provide will fall back to English:

i18n: {
  searchPlaceholder: 'Buscar', // Only override this one
  // Everything else uses English defaults
}

License

MIT © MagicPages