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.
Prose search
Section titled “Prose search”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-ignoreattribute. 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
Section titled “Symbol search”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:
{ "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.
Where it appears
Section titled “Where it appears”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:
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.
How matching works
Section titled “How matching works”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.
Searching several packages
Section titled “Searching several packages”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:
---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.