How to Add Site Search Without a Server
If the pages can be listed, the browser can do the reading. The first visitor pays a few seconds; everyone after them gets results as they type.
A site search box does not need a server if the pages can be listed. The widget fetches your sitemap in the visitor's own browser, reads the pages it names, builds the index in memory and keeps it in IndexedDB for a day. The install is one loader line plus one empty div. The first visitor on a device waits a few seconds while the index fills; every visit after that on the same device answers from the cache. It is the model Pagefind uses at build time, moved to runtime so that a WordPress.com, Tistory or Cafe24 site with no build step can have it.
Why search was the last thing a static site could not have
Everything else on a small site went static years ago. Pages are files, comments come from a widget, the newsletter form posts to a service. Search stayed different because someone has to read every page and keep a word list, and that someone used to be a server: a WordPress database, a hosted index, a Google custom search box that sends the visitor away. Pagefind changed the shape by doing the reading at build time and shipping the word list as static files next to the pages. Its limit is the build step itself. A Tistory blog or a Cafe24 shop has no build to hook into.
This widget keeps Pagefind's idea and moves the reading to the moment a visitor opens the page. The pages are already public and already served by your host, so the visitor's browser can fetch them the same way it fetches an image. What comes back is parsed, stripped to the readable text, split into words, and dropped into an inverted index in memory. That index is then written to IndexedDB, the browser's own database, under a key that names your site and the crawl settings. The next page view on the same device reads the cache instead of the pages.
What one page turns into
| On the page | What the index keeps | What it does with it |
|---|---|---|
<title>, the first <h1>, og:title | One title, the h1 first | The bold line of a result, boosted in ranking |
<meta name="description"> | Up to 300 characters | A ranking boost and the fallback excerpt |
main, article, [role=main] or data-bootsignal-body | The readable text, up to 12,000 characters | The excerpt, about 30 words around the densest match |
h2, h3 and h4 that carry an id | Heading text and where it sits | Sub-results, each an anchor link into that section |
nav, header, footer, aside, scripts, data-bootsignal-ignore | Nothing | Dropped before indexing, so the menu does not match every query |
<meta name="robots" content="noindex"> | Nothing | The page is skipped |
data-pagefind-body and data-pagefind-ignore attributes are honoured as well, so a site that already marked up its pages for Pagefind keeps that work.Words match by prefix, so typing wid already finds widgets, and English gets a light stemmer that folds searching into search. Korean, Japanese and Chinese have no spaces to split on, so those runs are indexed as character pairs, the same trick most CJK search engines fall back to when no dictionary is loaded. A page declares its language in <html lang>, and a visitor on a Korean page searches only the Korean pages whenever the index holds any.
The install
Get a site id for the domain the sitemap lists
Sign in, open the dashboard, add the hostname your pages actually live on, and copy the site id. The loader checks that id before it draws anything. The crawl only follows sitemap entries on the same origin as the page, and a sitemap that lists
www.example.comwhile the page runs onexample.comis folded onto the current origin rather than refused.Drop the loader and an empty mount
One script tag before the closing body tag, once per page, and one
divwhere the search box should sit. There is no thread id to fill in: search is a site-wide widget, not a per-page one.<script src="https://bootsignal.com/bootsignal.js" data-site-id="YOUR_SITE_ID" data-lang="en" defer></script> <div data-bootsignal="search"></div>Pick a box or a trigger
The default draws the input and the results in place.
data-mode="modal"draws a button that looks like a search box and opens a centered panel, which also answers to ⌘K on a Mac, Ctrl K elsewhere, and the/key when nothing else has focus. Use the modal in a header; use inline on a dedicated search page or in a sidebar.<div data-bootsignal="search" data-mode="modal" data-placeholder="Search the docs…"></div>Fence the crawl
Most sitemaps list tag pages, pagination and author archives that nobody wants as a result.
data-excludetakes comma-separated path prefixes to skip;data-includekeeps only the prefixes you name.data-max-pagescaps the crawl at 300 by default and 1,000 at most. If you can generate a JSON file of your pages yourself,data-indexpoints the widget at it and no crawl happens at all.<div data-bootsignal="search" data-include="/blog/,/docs/" data-exclude="/blog/tag/,/blog/page/" data-max-pages="400"></div>Point at the article, not the chrome
If your theme puts the post inside a
divwith nomainorarticleelement around it, adddata-bootsignal-bodyto thatdiv(or pass a selector withdata-body), and mark sidebars or related-post blocks withdata-bootsignal-ignore. Headings that carry anidbecome sub-results, and most themes already give them one.
What you just installed
A real widget. It reads this site's sitemap and indexes the product pages in your language right here in your browser. The first pass takes a few seconds, then the cache answers at once for a day. Try a word from any product name.
The widget on this page is indexing this site's product pages in your browser right now. The first pass shows the progress line; a reload on the same device answers from the cache.
One loader line, one empty div, and a sitemap you already have.
What it will not do, so you can decide before installing
- It reads at most 1,000 pages, 300 unless you raise the cap. A site with 5,000 posts gets the pages listed first, not all of them. Fence the crawl to the section that matters, or ship a
data-indexfile. - The first visit on a device pays for the crawl: a few seconds and one request per page, on the visitor's connection. The cache lives 24 hours by default (
data-ttl, in hours), and a stale cache is used at once while a fresh one is built behind it. - There is no typo tolerance.
wdgetfinds nothing, whilewidgfindswidget, because matching is by prefix and not by edit distance. - A page the visitor can open, the widget can index, and nothing else. The fetch carries that visitor's own cookies and the index never leaves their browser, so a members-only page is indexed for members and invisible to everyone else. Do not expect it to search pages you would not serve to that visitor.
- Sub-results need heading ids. A theme that renders
<h2>without anidstill gets the page as a result, just without the anchor links under it.
Where this sits next to Pagefind and a hosted index
Pagefind is open source and runs inside your build, which is the right choice for a Hugo, Astro or Eleventy site with a deploy pipeline: the index is prebuilt, the first search is instant, and there is nothing to subscribe to. Reach for this widget when there is no build to run, which is the situation on WordPress.com, Tistory, Cafe24, imweb, Blogger and Squarespace, or when you would rather not maintain the pipeline for a search box.
A hosted index such as Algolia's DocSearch does the crawling on its own servers and answers from there, so the first search on a fresh device is fast and forgiving of typos. The price is a service in the loop: an application or an account, a crawler that has to be let in, and a copy of your content on someone else's machine. This widget keeps the copy on the visitor's device and needs nothing beyond the pages you already serve.
Questions people ask
- Does the index update when I publish a new post?
- On the next crawl. Each visitor's cache lives 24 hours by default, so a new post appears for a returning visitor within a day and for a first-time visitor at once. Lower
data-ttlon a site that publishes several times a day, or callreindex()on the handle the widget returns. - How big can the site be before this stops making sense?
- The hard cap is 1,000 pages and the comfortable range is a few hundred. Past that, the first-visit crawl takes long enough to notice on a phone, and a
data-indexfile generated on your side, or a build-time tool such as Pagefind, is the better shape. - Is the search box slow on the first visit?
- The box appears at once and the results fill in as pages arrive. A query typed during the crawl re-runs every few pages, with a line under the input saying the index is still growing. On a 40-page site the crawl is over in a couple of seconds, and the cache answers every visit after that.
- Why do menu items and footer links never show up as matches?
- Because they are removed before indexing. Navigation, header, footer, aside and anything marked
data-bootsignal-ignoreordata-pagefind-ignoreis dropped. Otherwise a word that appears in the menu would match every page on the site with the same score. - Where does the subscription come in?
- The loader checks the site id against an active BootSignal subscription before it draws the widget, the same gate every paid widget uses. $20 per month or $200 per year per registered domain, with all 38 paid widgets included and nothing that scales with traffic, so a search box that is used ten thousand times costs the same as one used ten times.
The demo above is the real widget. The one on your site reads your sitemap instead.