Tom MacWright

tom@macwright.com

Hooking up search results from Astro Starlight in other sites

At Val Town, we recently introduced a command-k menu, that “omni” menu that sites have. It’s pretty neat. One thing that I thought would be cool to include in it would be search results from our documentation site, which is authored using Astro Starlight. Our main application is React, so how do we wire these things together?

It’s totally undocumented and this is basically a big hack but it works great:

Starlight uses Pagefind for its built-in search engine, which is a separate, very impressive, mildly documented open source project. So we just load the pagefind.js file that Starlight bakes, using an ES import, across domains, and then just use the pagefind API. So we’re loading both the search algorithms and the content straight from the documentation website.

Here’s an illustrative component, lightly adapted from our codebase. This assumes that you’ve got your search query passed to the component as search.

import { Command } from "cmdk";
import { useDebounce } from "app/hooks/useDebounce";
import { useQuery } from "@tanstack/react-query";

function DocsSearch({ search }: { search: string }) {
  const debouncedSearch = useDebounce(search, 100);
  
  // Use react-query to dynamically and lazily load the module
  // from the different host
  const pf = useQuery(
    ["page-search-module"],
    // @ts-expect-error
    () => import("https://docs.val.town/pagefind/pagefind.js")
  );

  // Use react-query again to actually run a search query
  const results = useQuery(
    ["page-search", debouncedSearch],
    async () => {
      const { results }: { results: Result[] } =
        await pf.data.search(debouncedSearch);
      return Promise.all(
        results.slice(0, 5).map((r) => {
          return r.data();
        })
      );
    },
    {
      enabled: !!(pf.isSuccess && pf.data),
    }
  );

  if (!pf.isSuccess || !results.isSuccess || !results.data.length) {
    return null;
  }

  return results.data.map((res) => {
    return (
      <Command.Item
        forceMount
        key={res.url}
        value={res.url}
        onSelect={() => {
          window.open(res.url);
        }}
      >
        <a
          href={res.url}
          onClick={(e) => {
            e.preventDefault();
          }}
        >
          <div
            dangerouslySetInnerHTML={{ __html: res.excerpt }}
          />
        </a>
      </Command.Item>
    );
  });
}

Pretty neat, right? This isn’t documented anywhere for Astro Starlight, because it’s definitely relying on some implementation details. But the same technique would presumably work just as well in any other web framework, not just React.