Multiple packages
One plugin instance documents any number of packages. Examples: the packages in a monorepo, a
package plus its plugins, or several released versions of one package. Each entry gets its own
pages, its own symbols.json, objects.inv and llms.txt, and its own sidebar group.
import starlight from '@astrojs/starlight';import { defineConfig } from 'astro/config';import starlightPydocs, { pydocsSidebarGroup } from 'starlight-pydocs';
export default defineConfig({ integrations: [ starlight({ title: 'My project', plugins: [ starlightPydocs({ packages: [ { name: 'mycore', search: ['../packages/core/src'] }, { name: 'myplugins', search: ['../packages/plugins/src'], docstringStyle: 'numpy' }, { name: 'legacy', base: 'api/legacy', source: { file: './dumps/legacy.json' } }, ], }), ], sidebar: [{ label: 'API reference', items: [pydocsSidebarGroup] }], }), ],});That builds /api/mycore/, /api/myplugins/ and /api/legacy/, and puts all three trees under
one API reference section in the sidebar. Almost every option applies per package. Packages
that use different docstring styles, griffe extensions or source links do not need a second plugin
instance. Only a handful of options are shared by all of them: the Sphinx inventories, the
symbolSearch, publishInventory and llmsTxt switches, the components overrides and
cacheDir.
Entries do not need to agree about where their model comes from either. legacy sets its own
base (the URL path its pages are generated under) instead of using the default. It also reads a
checked-in pre-generated dump instead of a
search path, so griffe never runs for it. The other two entries extract from source in the same
build.
This documentation site works this way. One starlightPydocs() call configures four entries:
demopkg (google style, extracted with a griffe extension), numpkg (numpy style), sphpkg
(sphinx style, from a pre-generated dump), and a second demopkg entry at its own base that reads
a dump instead of extracting.
The base identifies a package
Section titled “The base identifies a package”base is the URL path where an entry’s pages are generated. It defaults to api/<name>. base is
also the identity of that entry everywhere else: in the sidebar tree, in route matching, and in
the package prop of <Autodoc> and <SymbolSearch>. The Python import name is not an
identity. You can document the same package several times, one release per entry. This is how
several versions of one package live in one build.
Two rules follow from that.
Bases must not overlap
Section titled “Bases must not overlap”Each entry needs a distinct base. No base may contain another’s. api/mypkg and
api/mypkg_plugins are fine. api and api/mypkg are rejected: the pages of one would land
inside the other’s page tree:
packages[1].base: 'api/mypkg' overlaps packages[0].base ('api'); give each package a distinct baseAmbiguous names are refused, not guessed
Section titled “Ambiguous names are refused, not guessed”A name documented once resolves on its own. A name documented twice does not say which entry you
mean. So the components resolve their package prop as a base first, then as an import name:
package prop |
Resolves to |
|---|---|
package="1x/api/mypkg" |
The entry with that base. Always unambiguous. |
package="mypkg" |
The single entry documenting mypkg, or an error listing the candidate bases. |
omitted, on <Autodoc> |
The entry whose import name starts the dotted name prop, if only one does. |
omitted, on <SymbolSearch> |
The first configured entry. |
With mypkg documented at two bases, <Autodoc name="mypkg.Report" /> cannot pick one:
starlight-pydocs: <Autodoc name="mypkg.Report"> is ambiguous: 'mypkg' is documented at 2 bases. Set the package prop to one of these bases: 'api/mypkg', '1x/api/mypkg'.<SymbolSearch> does not throw an error. An ambiguous or unknown package renders no search box
at all, instead of searching a version of the package you did not ask for.
Sidebar placement
Section titled “Sidebar placement”By default, every package’s tree replaces the shared pydocsSidebarGroup placeholder, in
configuration order. This produces the single API reference section in the first example
above.
To put one package somewhere else, give it a placeholder of its own. Three steps do this: create
the placeholder with createPydocsSidebarGroup(), pass it to the package as sidebar.group, and
add it to your sidebar where that package belongs.
import starlight from '@astrojs/starlight';import { defineConfig } from 'astro/config';import starlightPydocs, { createPydocsSidebarGroup, pydocsSidebarGroup } from 'starlight-pydocs';
const pluginsApi = createPydocsSidebarGroup();
export default defineConfig({ integrations: [ starlight({ title: 'My project', plugins: [ starlightPydocs({ packages: [ { name: 'mycore', search: ['../packages/core/src'] }, { name: 'myplugins', search: ['../packages/plugins/src'], sidebar: { group: pluginsApi }, }, ], }), ], sidebar: [ { label: 'Core API', items: [pydocsSidebarGroup] }, { label: 'Plugin API', collapsed: true, items: [pluginsApi] }, ], }), ],});Each placeholder is replaced by the trees of the packages assigned to it. A package with its own
group leaves the shared placeholder to the rest. If you create a placeholder but never add it to
your sidebar, it is never substituted. That package’s pages still build, but they do not appear
in the sidebar.
sidebar.label renames a package’s group and sidebar.collapsed collapses it:
8 collapsed lines
import starlight from '@astrojs/starlight';import { defineConfig } from 'astro/config';import starlightPydocs, { pydocsSidebarGroup } from 'starlight-pydocs';
export default defineConfig({ integrations: [ starlight({ title: 'My project', plugins: [ starlightPydocs({ packages: [ { name: 'mypkg', search: ['../src'], sidebar: { label: 'mypkg (unstable)', collapsed: true, }, }, ],6 collapsed lines
}), ], sidebar: [{ label: 'API reference', items: [pydocsSidebarGroup] }], }), ],});Inside a package’s group, the first entry is the package’s own page. It is labelled with the
translated overview string (Overview in English). One entry per module follows, nested to
mirror the module tree.
Monorepo layouts
Section titled “Monorepo layouts”search paths resolve against the Astro project root. They can point anywhere, including sibling
directories, so the Python sources do not have to live inside the docs site:
Directorypackages/
Directorycore/
Directorysrc/
Directorymycore/
- …
Directoryplugins/
Directorysrc/
Directorymyplugins/
- …
Directorydocs/ the Astro project, where
astro.config.mjslives- …
starlightPydocs({ packages: [ { name: 'mycore', search: ['../packages/core/src'], sourceLink: { host: 'github', repo: 'you/repo', ref: 'main', root: '..' }, }, { name: 'myplugins', search: ['../packages/plugins/src'], sourceLink: { host: 'github', repo: 'you/repo', ref: 'main', root: '..' }, }, ],});root: '..' makes source links repository-relative from a docs/ subdirectory. See
Source links. Give a package several search paths when
its sources are split across directories.
Point each search path at the parent of the package directory, not at a whole checkout. The size
and modification time of every .py and .pyi file underneath goes into that package’s extraction
cache key. A path that covers unrelated code makes griffe re-run when code you are not documenting
changes.
Cross-package links
Section titled “Cross-package links”Docstring cross-references reach across packages. For
example, [Report][mycore.Report] in a myplugins docstring links into the core package’s pages,
with no inventory involved. Targets resolve in this order:
- the symbol index of the package being rendered;
- the symbol indexes of the other configured packages, in configuration order;
- the configured Sphinx inventories.
Entries sharing an import name are skipped at step 2. This stops one documented version of a package from linking into another version’s pages and silently mixing two APIs.
Type annotations do not cross packages. A name in an annotation resolves against the rendered package’s own scopes and symbol index, then against the inventories. Otherwise it renders as plain text, even when it names a class documented elsewhere on the same site.
Endpoints per package
Section titled “Endpoints per package”Each entry serves its own files under its own base:
/api/mypkg/symbols.json /1x/api/mypkg/symbols.json/api/mypkg/objects.inv /1x/api/mypkg/objects.inv/api/mypkg/llms.txt /1x/api/mypkg/llms.txtThere is no combined index across entries. Symbol search covers one entry at a time.
<SymbolSearch package="1x/api/mypkg" /> chooses which entry, by base or by an unambiguous import
name. A project that publishes several inventories must point consumers at each one.