Skip to content

Search

A search box in the navbar and a modal behind it, over an index Pagefind builds from your pages. It runs in the reader's browser: no service, no account, nothing to keep running.

Add it

Terminal
dotnet add package Nacara.Plugin.Search --prerelease
let theme =
    Theme.defaults
    |> Theme.navbarEnd [ NavbarDynamicWidget Search.trigger ]

let site =
    Site.create "My library"
    |> Markdown.register
    |> Search.register
    |> Theme.register theme

Two lines: Search.register builds the index and ships the modal, Search.trigger says where the box goes. Put the trigger anywhere the theme takes a widget.

Then build. The first build fetches the pinned Pagefind release into ~/.cache/nacara and indexes the output:

Terminal
✓ Search index built
✓ Built 22 pages, 22 written, 512 ms

What a reader gets

Ctrl+K (+K on a Mac) from anywhere opens the modal, and so does / when they are not typing in a field. Results are grouped by page, and by section within it: a click anywhere on a row follows it, walk them, opens the one they are on, esc closes. The modal lists these keys along its bottom.

The button shows the chord for the reader's keyboard, decided in their browser.

Nothing is downloaded until someone searches: not the index, not the modal's own code.

How it looks

The modal is Pagefind's own, drawn with the theme's tokens: the same surfaces, text, borders, focus ring and shadow as the rest of your site, so it follows the reader's light or dark choice.

Set Pagefind's own variables when you want something else - --pf-background, --pf-text, --pf-border, --pf-hover, --pf-modal-max-width, and the rest of what its documentation lists:

:root:root {
    --pf-modal-max-width: 52rem;
    --pf-border-radius: 0;
}

Copy the doubled :root:root. Pagefind's stylesheet loads the first time a reader opens the modal, after yours, and declares its defaults on a plain :root, so a plain :root of your own loses to it.

Options

OptionDefaultEffect
BinaryPathNoneUse your own pagefind instead of the pinned release
RootSelector"main"The element Pagefind indexes as the page body

RootSelector keeps the navbar and the sidebar out of every result. Change it if your layout puts content somewhere else.

Using your own pagefind

Pagefind is fetched once per machine and kept, so only the first build downloads it. Point BinaryPath at your own copy and nothing is fetched:

|> Search.registerWith (fun options ->
    { options with
        BinaryPath = Some "/opt/pagefind/pagefind"
    }
)

Deploying

The index lives in pagefind/ beside your pages and deploys with them. Nothing else is needed: no server-side component, no external service.

A site served from a subdirectory finds its index there too: the trigger is rendered from your site, so /project/ looks in /project/pagefind/.

Reference

Every function and option of it, signature by signature: Search.

Edit this page