Skip to content

Search

Two searches cover the generated pages, and they answer different questions. Type a phrase from a docstring into the site’s search dialog, and you get the page that docstring appears on. Type Report.generate into the plugin’s own search box, and you get that object’s heading. The first search is Pagefind, the static full-text search that ships with Starlight. It indexes rendered prose. The second search is <SymbolSearch>, a component this plugin provides. It searches the API surface by name.

Under Starlight, generated pages render the standard page shell. Pagefind indexes them with no configuration. A search for a sentence from a docstring finds the module page it belongs to, and the result links into the page like any other. This site’s end-to-end test suite checks this: a search for generate_report finds /api/demopkg/.

Two things follow from Pagefind indexing whole pages:

  • A module page’s search result covers every member on that page. A query that matches a method’s prose takes the reader to the module page, not to the method’s anchor.
  • To exclude API pages from search, use the same method as for any Starlight page: add Pagefind’s own data-pagefind-ignore attribute. Apply it from a component override if you want to drop part of a page instead of the whole page.

Pagefind builds its index from the built HTML, so there is nothing to search during development. Starlight’s search dialog reports that search only works in production builds. To try a query, run astro build and then astro preview. Symbol search does work in astro dev, because its index is a route that the dev server serves live.

A plain Astro site has no Pagefind unless you add it. The symbol search below works either way.

Symbol search has two pieces: an endpoint that publishes the index, and a custom element that queries it in the browser. It is on by default.

The endpoint is served next to each package’s pages, at <base>/symbols.json. It is the only pydocs JSON that a browser ever fetches. The griffe dumps stay on disk:

api/demopkg/symbols.json
{
"package": "demopkg",
"base": "api/demopkg",
"generated": "2026-08-13T09:00:00.000Z",
"symbols": [
{ "path": "demopkg.Report", "kind": "class", "page": "api/demopkg", "anchor": "demopkg.Report", "brief": "A named collection of scored sections." },
{ "path": "demopkg.report", "kind": "module", "page": "api/demopkg/report", "anchor": "", "brief": "Report classes and the functions that build them." }
]
}

Each entry describes one documented object: its dotted path, its kind, the page slug it lives on, its anchor, and the first line of its docstring. The anchor is empty for a module, because a module is a page rather than a heading. The element turns page and anchor into a URL, so the payload stays base-agnostic.

The search box appears automatically on each package’s root page: the page at the package’s base, which documents the package’s top-level module. This is where a reader arrives when looking for a name. One box per package keeps the search box off every module page below.

Put it anywhere else by hand:

src/content/docs/reference.mdx
import { SymbolSearch } from 'starlight-pydocs/components';
<SymbolSearch />
<SymbolSearch package="numpkg" class="my-search" labels={{ searchLabel: 'Find a symbol' }} />
Prop Type Default Meaning
package string the first package Which configured package to search
class string none Extra class on the custom element
labels StringOverrides site translations Label overrides for this instance

labels takes the same keys as every other component. See Internationalisation.

The box below is a hand-placed example, and it is live rather than a screenshot: it searches this site’s demopkg pages. Type report into it and follow a result:

Type part of a name or a dotted path

With symbolSearch: false, the endpoint is not served. The component renders nothing in that case, so a hand-placed box never appears without its index.

The index loads on first focus, not on page load, so a page nobody searches costs nothing. Matching runs in the browser and ignores case. It ranks hits in a fixed, explainable order, not a fuzzy one: an exact short name first, then a short-name prefix, then a prefix of the full dotted path, then a substring of the name, then a substring of the path. Ties go to the shorter path first, so demopkg.Report sorts above demopkg.report.Report. Remaining ties go to alphabetical order, which keeps results stable between keystrokes. Because the whole dotted path is matched, report.gen finds demopkg.report.generate_report. The list shows at most 25 results, grouped by kind, using the same translated kind labels as the badges.

You can operate the element with the keyboard: ArrowDown and ArrowUp move through the results, Enter follows the active result, and Escape closes the list. It is a combobox with aria-expanded, aria-activedescendant, and one role="option" per result. It defines itself once and queries only its own subtree, so several boxes can appear on one page, and Astro’s view transitions still work.

Each package has its own index. Each box searches one package. For a site that documents several packages, either give the reader one box per package or fetch the endpoints yourself:

src/pages/search.astro
---
const bases = ['api/mypkg', 'api/otherpkg'];
const indexes = await Promise.all(bases.map((base) => fetch(new URL(`${base}/symbols.json`, Astro.site)).then((r) => r.json())));
---

For anything more complex, use the Content Layer loader. It gives you the same surface as collection entries. You can query it at build time and render the results however you like.