# 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.

```js title="astro.config.mjs" {12-14}
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](/starlight-pydocs/guides/pregenerated-dumps/) 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

`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 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:

```js title="astro.config.mjs" collapse={1-8, 21-26} {15-18}
export default defineConfig({
  integrations: [
    starlight({
      title: 'My project',
      plugins: [
        starlightPydocs({
          packages: [
            {
              name: 'mypkg',
              search: ['../src'],
              sidebar: {
                label: 'mypkg (unstable)',
                collapsed: true,
              },
            },
          ],
        }),
      ],
      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

`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:

- packages/
  - core/
    - src/
      - mycore/
        - …
  - plugins/
    - src/
      - myplugins/
        - …
- docs/ the Astro project, where `astro.config.mjs` lives

```js title="docs/astro.config.mjs" {5,10} "root: '..'"
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](/starlight-pydocs/guides/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

[Docstring cross-references](/starlight-pydocs/guides/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:

1. the symbol index of the package being rendered;
2. the symbol indexes of the other configured packages, in configuration order;
3. 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

Each entry serves its own files under its own base:

```text
/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.txt
```

There 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.

[Versioned docs](/starlight-pydocs/guides/versioned-docs/)
  [Pre-generated dumps](/starlight-pydocs/guides/pregenerated-dumps/)
  [Cross-references](/starlight-pydocs/guides/cross-references/)