Uniweb.iov2.1.0
FeaturesHow It WorksWorkflowBlogDocs
Get Started
  • Getting Started
  • Authoring
    • Setting Up Your Site
    • Writing Content for Your Uniweb Site
    • Working with Records
    • Theming Your Uniweb Site
    • Linking
    • Site Search
    • Content Recipes
    • Translating Your Site
    • Predicates and Saved Views
  • Development
  • Reference
  • Architecture

Site Search

Uniweb sites have built-in full-text search. By default it runs entirely in the browser against an index generated at build time — no server, no service, no crawler.

Which provider serves results is a site setting, not something the foundation decides. A search UI written against the framework works unchanged whether results come from a downloaded index or from a server. That is the same arrangement data fetching uses: the site declares the source, components read the results.

Quick Start

Search works out of the box. If your foundation includes a search UI component (like the academic template's SearchModal), users can search your site immediately.

To explicitly enable and configure search, add to site.yml:

services:
  search:
    enabled: true

How It Works

  1. Build time: Content is extracted from all pages and sections
  2. Index generation: A search-index.json file is created in your build output
  3. Runtime: The search client loads the index on first use and caches it
  4. Search: The query is ranked against the index in the browser, as described below — by the same engine a host that answers search uses, so a site's results come back in the same order wherever it is served

The index is cached in localStorage and revalidated against the server on each load, so a rebuilt site never answers from the copy a visitor cached earlier. When the index is unchanged the check costs a few hundred bytes rather than a re-download.

How results are ordered

Results are ranked by relevance (BM25F):

  • A word in the title counts for more than the same word in the body, and a page counts for more than one of its sections.
  • Rare words count for more than common ones. A word that appears on nearly every page barely affects the order, so there is no stop-word list to maintain, in any language.
  • The query's words together as a phrase rank a result higher.
  • The last word is completed as you type: insta finds install and installation.
  • A misspelling is corrected only when nothing matched. A query with no exact hits falls back to words one edit away — fomr finds form — so a near-miss never outranks a page that uses the word you typed.
  • Accents and case are ignored (café finds cafe), and text in scripts written without spaces between words — Chinese, Japanese, Thai — is indexed in pairs of characters, so a query can find a word inside a sentence.

A word that appears nowhere in the site doesn't sink a query: the other words still rank. Words are matched as written, with no stemming — running does not find ran.

Providers

A provider is what actually answers a query.

# site.yml
services:
  search:
    provider: index        # the default; writing it changes nothing — omit it unless
                           # you are switching AWAY from the local index
ProviderWhat it doesTrade-off
index (default)Downloads search-index.json and ranks it in the browserFree and works on any host, including a plain static one. Corrects typos. Can only contain what existed at build time.
endpointQueries a server-side search APICan index content that isn't in your files — records fetched from an API — and can be re-indexed without rebuilding the site. Needs a host that serves one.
any other nameA search transport supplied by your foundationFully open — Typesense, Meilisearch, Pagefind, a vendor API

Using a server endpoint

services:
  search:
    provider: endpoint
    endpoint: _search      # required — there is no default

endpoint is required. A provider with nowhere to send a query has nothing to do, so search behaves like every other service here: you name the target, or the site has no search. (Until 0.16 an omitted endpoint fell back to _search. It no longer does — a path a host serves is that host's to name, and a framework guess competes with it.)

It is resolved relative to your site's base path, which is what makes one spelling work everywhere. On a site at the root _search resolves to /_search; under base: /docs/ it becomes /docs/_search; on a site served from a subpath it follows that subpath. Give an absolute https://… URL to point at a search service on another origin.

A host that serves your site may offer search itself: search: true under services: asks for it, and the host supplies the address. A server endpoint of your own is the other way round — it asks your host to leave its own search off, so yours answers.

The response envelope is read leniently — { results: [...] }, { hits: [...] }, { items: [...] }, or a bare array all work — so a self-hosted search backend usually needs no adapter.

Graceful degradation

If a declared provider fails — the endpoint is unreachable, or the site moved to a host that doesn't serve one — the client falls back to the local index when one exists, and otherwise returns no results with a console warning. A search box never throws at a visitor.

This means moving a site between hosts is safe: search quietly returns to the built-in index.

Search results

Every provider returns the same result shape, so a search UI is written once.

Always present — safe to render without checking:

FieldMeaning
idStable identifier for the hit
typepage, section, or record
routePage route the hit belongs to
hrefWhere to navigate — includes the #anchor when there is one
titleThe hit's own title
pageTitleTitle of the containing page
excerptShort plain-text summary
snippetHtmlMatching text with <mark> around the query terms

Present when the provider can supply it — null otherwise:

sectionId, anchor, description, component, snippetText, matches, group, item

Whether one of these arrives is a deployment fact, not a content fact — the same site yields item (a record's fields; group names the query it came from) from a server provider and null from the local index, while matches goes the other way. Render them defensively:

{result.item?.image && <img src={result.item.image} alt="" />}

snippetHtml is HTML. Render it through kit's SafeHtml, never as plain text.

Configuration

Basic Configuration

# site.yml
services:
  search:
    enabled: true

Full Configuration

services:
  search:
    enabled: true

    # What to include in the index
    include:
      pages: true        # Page titles and descriptions
      sections: true     # Section content
      headings: true     # Heading text
      paragraphs: true   # Paragraph text
      links: true        # Link labels
      lists: true        # List item text

    # What to exclude
    exclude:
      routes:            # Routes to skip (prefix match)
        - /admin
        - /draft
      components:        # Component types to skip
        - CodeBlock
        - RawHtml

All include options default to true. Exclusions default to empty arrays.

Disabling Search

services:
  search:
    enabled: false

Or simply omit the search configuration—search is enabled by default.

Foundation Requirements

To use search, your foundation needs a search UI component that uses the search client from @uniweb/kit — nothing to install beyond kit. The engine that ranks the local index is loaded on demand, only when a site actually queries it, so a site using provider: endpoint never downloads it.

The academic template includes a search UI ready to use.

Nothing in a search UI needs to know which provider is active. If you want to show it — a "live results" badge, say — the client exposes getProviderName(), which reports the active provider (so it reads index after a fallback, not what was declared).

What Gets Indexed

Pages

  • Title (from page.yml)
  • Description
  • Keywords (from SEO config)

Pages are weighted higher in search results than sections.

Sections

  • Headings (first H1 becomes the section title)
  • Paragraphs
  • Link labels
  • List items

Excluded Content

The following are automatically excluded:

  • Layout panels (header, footer, sidebars)
  • Pages marked with seo.noindex: true
  • Routes and components in search.exclude

Keyboard Shortcuts

The academic template's SearchModal includes keyboard support:

ShortcutAction
Cmd/Ctrl + KOpen search
↑ / ↓Navigate results
EnterGo to selected result
EscapeClose search

Multi-Locale Support

For sites with multiple locales, separate search indexes are generated:

dist/
├── search-index.json      # Default locale
├── es/
│   └── search-index.json  # Spanish
└── fr/
    └── search-index.json  # French

The search client automatically uses the correct index based on the active locale.

Performance

  • Index size: Typically 20-50KB for small/medium sites
  • Caching: Index is cached in memory and localStorage
  • Lazy loading: the ranking engine is loaded only when a site queries its local index
  • Preloading: Optional client.preload() for instant first search

For large sites (hundreds of pages), consider:

  • Excluding verbose components (like full article bodies)
  • Using route exclusions for low-value content

See Also

  • Site Configuration — Full site.yml reference
  • Content Structure — How content is organized
  • Internationalization — Locale-specific search indexes

On this page

Uniweb.io

A developer toolkit that lets non-developers use your React components.

Product

  • Features
  • How It Works
  • Workflow
  • Uniweb Cloud

Developers

  • Get Started
  • Documentation
  • Blog
  • GitHub
  • NPM Package

© 2026 Uniweb. Open source framework.

  • Getting Started
  • Authoring
    • Setting Up Your Site
    • Writing Content for Your Uniweb Site
    • Working with Records
    • Theming Your Uniweb Site
    • Linking
    • Site Search
    • Content Recipes
    • Translating Your Site
    • Predicates and Saved Views
  • Development
  • Reference
  • Architecture