The component library
How the composer discovers your sections, what it reads out of each file, and the override block for the facts your source cannot state.
The page composer’s library lists your components. It can only ever name what already exists, which is the same promise the entry editor makes about your schema — no config.yml tax, no registry to keep in step with a directory.
Discovery
Two directories, named in src/admin.config.ts:
components: {
sections: "src/components/sections",
layouts: "src/layouts",
}
Both are walked recursively — files before subdirectories, each sorted — and read at request time. Both must exist even if empty, both are path-contained (a .. would list your file tree on a screen), and both must live under src/, because a composed page imports through @/* and that is the only alias reaching an arbitrary directory.
A directory becomes a group in the library, so an emptied folder hides along with its children.
Sibling directories of your sections directory that also hold .astro files are listed as not composable, so you can see that they were considered rather than wondering where they went. That only applies when the parent is not src/ itself.
What it reads out of each file
interface Props — the declared props and their types. This is the contract half, and it is what a component author controls.
The Astro.props destructure — const { wide = true } = Astro.props. The frontmatter’s last word on a prop, so a control nobody has touched can show what the component actually renders. A quoted string, a number, true or false is taken as the default; an expression, an array or an object is not. An enum takes a default only when the literal is one of its own options.
<slot /> — a component that renders one is listed and tagged. It is still addable; the tag exists because a section with a slot the composer cannot fill would otherwise be a silent dead end.
The composer never imports one of your components. An .astro component’s props do not survive to runtime, so importing would buy nothing and would breach the package boundary besides.
How much this actually covers
Measured against four real Astro themes when the feature was designed: for AstroWind, one of twenty-two widgets was readable from its source; for ScrewFast, four of nineteen. But per field, of what is in the file, the parser reads 70–85%.
So coverage is bimodal per component, not per field: a component either declares its props inline and is almost fully readable, or centralises its types in another file and is barely readable at all. That shape is what the override block exists for. Resolving imported types with the TypeScript compiler was priced and refused — it buys only what a declaration already covers.
Overrides
components: {
sections: "src/components/sections",
layouts: "src/layouts",
overrides: {
CallToAction: { wireframe: "cta" },
Features: {
category: "Content",
wireframe: "list",
props: [
{ kind: "string", name: "title", label: "Title", required: true, widget: "text" },
{
kind: "array",
name: "items",
label: "Items",
required: true,
of: {
kind: "object",
name: "items",
label: "Item",
fields: [
{ kind: "string", name: "name", label: "Name", required: true, widget: "text" },
{ kind: "string", name: "detail", label: "Detail", required: true, widget: "textarea" },
],
},
},
],
},
},
}
Keyed by the component’s name — its filename without .astro. Every field is optional; an override states only what is wrong.
| Key | Corrects |
|---|---|
label |
the library card’s title; defaults to the component’s name |
category |
its grouping; defaults to Sections |
wireframe |
the placeholder drawing; defaults to the generic block |
props |
the component’s props, declared, when the source cannot be read |
Leave a component out entirely and it still appears, drawn with the generic wireframe and whatever its interface Props declares. You add an entry only to correct or complete it.
Wireframes are derived from the component’s name where that works — Hero.astro finds hero. CallToAction finds nothing, because “calltoaction” is not a drawing, hence the one-line override above. Features finds nothing either, and what that section actually looks like is a list.
props is the declaration road. Features types its items as an imported interface, so the parser reaches title and stops. One entry and the composer can edit the cards. This is the shape to copy when your own sections centralise their types in a types.ts.
On an array, required means the prop must be present, not that the list must be non-empty. items={[]} renders an empty list; <Features /> throws at render and fails your build. Adding a required array seeds it with [] so the requirement is satisfiable the moment the section exists.
Layouts and the SEO tab
A page layout can declare which of its props are the search title, the search description and the link picture — the same five key names a collection uses, because it is one declaration shape rather than two:
BaseLayout: {
titleField: "title",
descriptionField: "description",
socialImageField: "image",
socialTitleField: "socialTitle",
socialDescriptionField: "socialDescription",
},
That is what puts the SEO tab on the composer’s rail, over a layout’s props instead of a schema’s fields.
It is a declaration rather than a derivation for one visible reason: this layout spells its picture prop image, and the next adopter’s will not. Leave the block out and the composer simply shows no SEO tab for that layout, which is the honest answer — a control addressing socialImage on a layout that takes image would write a prop the component ignores, and that is a page that looks edited and is not.
Name collisions
Two components with the same name in different directories are reported in the library. The declaration side already refuses a duplicate key.
Troubleshooting
A component shows no props at all. Its interface Props is not in the file, or its types are imported. Declare them in an override.
A control shows the wrong default. The Astro.props destructure default is only read for a literal. const { size = SIZES.md } is an expression and carries nothing.
An enum control has no default although the component sets one. The literal has to be one of the enum’s own options. A mistyped default carries nothing rather than adding an option.
A component is missing from the library. It is not under one of the two configured directories, or those directories are wrong in admin.config.ts. Both paths are repo-relative and must be under src/.
A section renders as a labelled placeholder in the canvas. Its file is gone, or it was added in a commit this deployment’s bundle predates. The row survives on purpose, so the counter does not lie.