Skip to content
duxt

Meilisearch

Add the official companion layer for atomic build-time indexing and optional browser search.

The official @kirchdev/duxt-meilisearch companion layer consumes the entire duxt:search:records snapshot. It keeps URL-derived IDs, normalized plaintext, source, version and locale, including generated sections. Meilisearch is optional; the built-in search needs no service.

pnpm add @kirchdev/duxt-meilisearch
export default defineNuxtConfig({
  extends: ['@kirchdev/duxt', '@kirchdev/duxt-meilisearch'],
  duxtMeilisearch: {
    host: process.env.MEILISEARCH_HOST,
    index: 'docs',
    searchKey: process.env.MEILISEARCH_SEARCH_KEY,
    syncKey: process.env.MEILISEARCH_SYNC_KEY,
    taskKey: process.env.MEILISEARCH_TASK_KEY,
    sync: {
      enabled: process.env.MEILISEARCH_SYNC_ENABLED === 'true',
      deployment: process.env.DUXT_DEPLOYMENT === 'production'
        ? 'production' : 'preview'
    }
  }
})

Set both production opt-ins only in your production publication job. Possessing syncKey alone enables no writes. Development, Nuxt prepare and recognized PR or preview contexts skip synchronization; other CI systems must map preview contexts to deployment: 'preview'. Cloudflare Pages also needs DUXT_PRODUCTION_BRANCH to match CF_PAGES_BRANCH.

Use a dedicated docs index. Give the private build key indexes.create, indexes.get, indexes.delete, indexes.swap, settings.update, documents.add, and tasks.get for docs and docs__duxt_*. The layer builds a unique staging index, waits for settings and document-import tasks, swaps it atomically with docs, waits for successful publication, then removes the prior snapshot. Later builds remove deleted pages, and empty builds clear the index. Failed imports leave the previous live snapshot in place. Uncertain swaps retain staging rather than deleting possibly published data; cleanup failures are reported. Serialize publication jobs when build order matters. The first sync creates an empty target after staging is complete.

The private key never enters browser runtime config. Browser integration is separate: omit searchKey to retain built-in search while synchronizing. To connect the existing dialog, create a separate key with only the search action and only the configured docs index. The default search key covers all indexes; do not use it. Never use a master or admin key in public config. The provider rejects reuse of a known build key; opaque key strings do not reveal permissions, so scoped provisioning is your responsibility. Browser-only use needs no private build key. Allow your site's origin in the service's CORS policy.

Search keeps each artefact's selected/default version and the content-language fallback chain. A translated page suppresses its original; a page lacking that translation remains reachable in the original language. The companion reads a cached page-path inventory through Content and excludes covered pages within each source/version/locale scope before Meilisearch ranks and limits results. The provider's filter-only page field holds the URL without its anchor; hidden sections cannot consume the engine's 1,000-hit window. Synchronize once before using browser search with an index created by an earlier provider build. Service errors yield an empty result and a console warning. HTTP requests time out after 30 seconds; build tasks have a five-minute budget, configurable through sync.taskTimeoutMs (at most one hour).

This package has its own release version and changelog. It is a separate Nuxt layer, with no SDK, credentials or provider configuration added to duxt. The browser helper is exported at @kirchdev/duxt-meilisearch/search; explicit build tooling may use @kirchdev/duxt-meilisearch/sync with the same opt-in.

The service guarantees come from Meilisearch's atomic index swap and asynchronous task API. See creating a scoped API key for the separate browser credential.

Meilisearch 1.54.3 hides the global indexSwap task (indexUid: null) from index-restricted keys, even when they cover both swapped indexes. With a scoped write key, supply a separate private taskKey with only actions: ['tasks.get'] and indexes: ['*']. It is used only for task polling and never enters public config. Without it, syncKey must itself be able to read global tasks. A hidden 404 fails publication confirmation and retains staging; it is never taken as success. This behavior was verified against the real engine.

Was this page helpful?