Home

5 min read

Free Website Static Search

Table of Contents

Introduction

In this guide, we will walk you through the steps to add a static search functionality to your website using Pagefind and the Spider CLI. This setup can be completed in under 5 minutes.

Prerequisites

Before starting, ensure you have Rust installed on your machine.

Installation

First, install the necessary modules:

cargo install spider_cli
cargo install pagefind

Indexing Your Website

Replace localhost:3000 with your website’s URL, whether it’s local or remote.

To index your website with Spider and Pagefind, and create the necessary files in your public directory, run the following command:

# download all the contents to disk
spider --url ${DOMAIN_NAME:-https://spider.cloud} download
# run pagefind index
pagefind --source _temp_spider_downloads --bundle-dir public/_pagefind
# copy files to public folder
cp -R _temp_spider_downloads/public/_pagefind public
# cleanup dir
rm -R _temp_spider_downloads/public

Alternatively, you can use the auto-pagefind package. First, install it:

cargo install auto-pagefind

Then run the following command to index:

auto-pagefind -u http://localhost:3000

Example of the output:

Pagefind spider indexing example in Visual Studio Code indexing a full website fast

With the power of Spider and Pagefind you can fully index websites with millions of pages within seconds.

Importing Pagefind Script and CSS

To load the Pagefind script in your HTML, add the following code to your <head> section:

<script src="/_pagefind/pagefind-ui.js" type="text/javascript" defer></script>
<link href="/css/_pagefind.css" rel="stylesheet" />
<link href="/_pagefind/pagefind-ui.css" rel="stylesheet" />

The custom _pagefind.css file:

:root {
    --pagefind-ui-scale: 1;
    --pagefind-ui-primary: #034ad8;
    --pagefind-ui-text: #393939;
    --pagefind-ui-border-width: 2px;
    --pagefind-ui-border-radius: 8px;
    --pagefind-ui-image-border-radius: 8px;
    --pagefind-ui-image-box-ratio: 3 / 2;
    --pagefind-ui-font: sans-serif;
    --pagefind-ui-placeholder: #000;
    --pagefind-ui-link: #1152d2;
}

:root.light {
    --pagefind-ui-scale: 1;
}

:root.dark {
    --pagefind-ui-scale: 1;
    --pagefind-ui-primary: #eeeeee;
    --pagefind-ui-text: #eeeeee;
    --pagefind-ui-background: #152028;
    --pagefind-ui-border: #152028;
    --pagefind-ui-tag: #152028;
    --pagefind-ui-placeholder: #fff;
    --pagefind-ui-link: rgba(59,130,246, 1);
}


.pagefind-ui__results-area  > .pagefind-ui__results {
    max-height: 50vh;
    overflow: auto;
    text-align: left;
}

.pagefind-ui__result > .pagefind-ui__result-inner > .pagefind-ui__result-title > a.pagefind-ui__result-link {
    color: var(--pagefind-ui-link);
}

.pagefind-ui__form > .pagefind-ui__search-clear:hover {
    color: var(--pagefind-ui-placeholder);
}

.pagefind-ui__form > .pagefind-ui__drawer > .pagefind-ui__results-area {
    margin-top: auto;
}

.pagefind-ui__search-input::placeholder {
    color: var(--pagefind-ui-placeholder);
    opacity: 1;
}

.pagefind-ui__result > .pagefind-ui__result-inner > .pagefind-ui__result-excerpt {
    padding: 0.2em;
    margin-top: auto;
    text-overflow: ellipsis;
    overflow: hidden;
}

.pagefind-ui {
    --pagefind-ui-scale: 0.75;
    --pagefind-ui-background: hsl(var(--input));
    --pagefind-ui-text: hsl(var(--foreground));
    --pagefind-ui-border: hsl(var(--border));
    --pagefind-ui-border-width: 1px;
    --pagefind-ui-border-radius: var(--radius);
    --pagefind-ui-font: var(--aw-font-inter), sans-serif;
    width: 100%;
}

.pagefind-ui .pagefind-ui__drawer:not(.pagefind-ui__hidden) {
    position: absolute;
    min-width: 50vw;
    /* left: 0; */
    right: 0;
    margin-top: 0px;
    z-index: 9999;
    padding: 0 2em 1em;
    overflow-y: auto;
    box-shadow:
        0 10px 10px -5px rgba(0, 0, 0, 0.2),
        0 2px 2px 0 rgba(0, 0, 0, 0.1);
    border-bottom-right-radius: var(--pagefind-ui-border-radius);
    border-bottom-left-radius: var(--pagefind-ui-border-radius);
    background-color: var(--pagefind-ui-background);
}

.pagefind-ui .pagefind-ui__result-link {
    color: var(--pagefind-ui-primary);
}

.pagefind-ui .pagefind-ui__result-excerpt {
    color: var(--pagefind-ui-text);
}

@media (any-pointer: coarse) {
    .pagefind-ui__result-title {
      text-align: center;
    }
}

Implementing Search Component in React

You can use a pre-made React component that is fully customizable. First, install the package:

npm i pagefind-react --save

Then, use the following React component:

import { PageFind } from "pagefind-react";

<PageFind />

Or you can copy the code below and use your own file type like Astro or Svelte.

import { type SyntheticEvent, useEffect, useRef, useState, startTransition } from 'react';
import { Search } from 'lucide-react';

// Re-usable classes
const PAGE_FIND_DRAWER_ID = '.pagefind-ui__drawer';
const PAGE_FIND_FORM = '.pagefind-ui__form';

export const PageFind = ({ bundlePath, targetID }: { bundlePath?: string; targetID?: string }) => {
  const [loaded, setLoaded] = useState<boolean>(false);

  const pfFind = useRef<HTMLInputElement | null>(null);

  const textSearch = 'Search';
  const searchText = targetID ? `${textSearch}...` : textSearch;

  useEffect(() => {
    if (pfFind.current) {
      pfFind.current.placeholder = searchText;
    }
  }, [pfFind, searchText]);

  useEffect(
    (_?: SyntheticEvent<HTMLInputElement>) => {
      if (!loaded) {
        let observer: MutationObserver;

        const PagefindUI =
          // @ts-ignore
          typeof window.PagefindUI !== 'undefined' && window.PagefindUI;

        if (PagefindUI) {
          try {
            const findID = targetID ? `#${targetID}` : '#search';

            new PagefindUI({
              element: findID,
              resetStyles: false,
              showImages: false,
              showEmptyFilters: false,
              bundlePath: bundlePath ?? '/_pagefind_guides/',
            });

            startTransition(() => {
              setLoaded(true);
            });

            const pagefindDrawer = document.querySelector(
              findID ? `${findID} ${PAGE_FIND_DRAWER_ID}` : PAGE_FIND_DRAWER_ID
            );

            if (pagefindDrawer) {
              pagefindDrawer.className = `${pagefindDrawer.className} absolute ${
                targetID ? 'z-30 min-w-[90vw] -right-16 md:min-w-[40vw]' : 'z-20'
              } bg-white dark:bg-black px-4 border dark:border-gray-600 shadow-xl mt-2 py-2 rounded md:right-0`;

              const pagefindForm = document.querySelector(
                `${findID} .pagefind-ui > ${PAGE_FIND_FORM}`
              );

              if (pagefindForm) {
                pagefindForm.ariaLabel = targetID ? `${textSearch} Website` : textSearch;
              }

              pfFind.current = document.querySelector(
                `${findID} .pagefind-ui > ${PAGE_FIND_FORM} > .pagefind-ui__search-input`
              );

              let focused = 'no';

              const onFocusInput = () => {
                if (
                  focused === 'backdrop' &&
                  pagefindDrawer.className.endsWith('pagefind-ui__hidden')
                ) {
                  pagefindDrawer.className = pagefindDrawer.className.replace(
                    'pagefind-ui__hidden',
                    ''
                  );
                }
                focused = 'active';
              };

              const onBlurFocusInput = () => {
                focused = 'blured';
              };

              const domCaptureClick = (e: MouseEvent) => {
                const targetElement = e && (e.target as HTMLElement | null);

                if (targetElement) {
                  if (
                    typeof targetElement.className === 'string' &&
                    targetElement.className.startsWith('pagefind-ui__')
                  ) {
                    return;
                  }
                  if (
                    pagefindDrawer.className &&
                    !pagefindDrawer.className.endsWith('pagefind-ui__hidden')
                  ) {
                    pagefindDrawer.className = `${pagefindDrawer.className} pagefind-ui__hidden`;
                    focused = 'backdrop';
                  }
                }
              };

              if (pfFind.current) {
                document.addEventListener('click', domCaptureClick);
                pfFind.current.addEventListener('focus', onFocusInput);
                pfFind.current.addEventListener('blur', onBlurFocusInput);
                pfFind.current.placeholder = searchText;
                pfFind.current.ariaLabel = searchText;
              }

              const guidesContainer = document.getElementById('articles-container');

              const callback = () => {
                const links: NodeListOf<HTMLAnchorElement> = pagefindDrawer.querySelectorAll(
                  '.pagefind-ui__result-link'
                );

                if (guidesContainer) {
                  guidesContainer.ariaHidden = links.length ? 'true' : 'false';
                }

                for (const link of links) {
                  link.href = link.href.replace('.html', '');
                }
              };

              observer = new MutationObserver(callback);

              observer.observe(pagefindDrawer, {
                attributes: false,
                childList: true,
                subtree: true,
              });

              return () => {
                if (loaded) {
                  if (pfFind.current) {
                    document.removeEventListener('click', domCaptureClick);
                    pfFind.current.removeEventListener('focus', onFocusInput);
                    pfFind.current.removeEventListener('blur', onBlurFocusInput);
                  }
                  if (observer) {
                    observer.disconnect();
                  }
                }
              };
            }
          } catch (e) {
            console.error(e);
          }
        }
      }
    },
    [loaded, setLoaded, textSearch, searchText, bundlePath, targetID, pfFind]
  );

  const preId = targetID ? `${targetID}_pre` : 'target_pre';

  return (
    <>
      {!loaded ? (
        <>
          <label className="sr-only" htmlFor={preId}>
            {searchText}
          </label>
          <div className="flex relative text-xl font-bold place-items-center">
            <Search className="grIcon absolute left-5 w-4 h-4 pointer-events-none text-gray-500 dark:text-gray-300" />
            <input
              className={`pagefind-ui pagefind-ui__search-input ${
                targetID ? 'max-h-[45px] text-sm' : 'max-h-[45px]'
              } px-12 py-6 text-sm w-full appearance-none rounded-[var(--pagefind-ui-border-radius)] bg-[var(--pagefind-ui-background)] placeholder-gray-300 dark:placeholder-gray-400`}
              placeholder={searchText}
              type="search"
              id={preId}
              readOnly
            />
          </div>
        </>
      ) : null}
      <div id={targetID ?? 'search'} className={loaded ? undefined : 'sr-only'}></div>
    </>
  );
};

Conclusion

By following these steps, you now have a fully functional static search feature on your website leveraging client-side web assembly for rapid performance. Your search functionality is now ready to provide users with a seamless search experience. PS: the search on this website is powered by this!

Build now, scale to millions