Introduction
A Standard module is a self-contained vertical slice — routes, styles,
components, config, and behavior, dropped into modules/ and wired up
automatically. No central router to edit, no import to add anywhere else,
no manual registration step.
Philosophy: A module should do one thing and declare everything it
needs. No hidden globals, no manual registration. The system finds it,
loads it, and merges it in.
The shape of a module
Every module is a folder with an index.module.js (or .ts) at its root:
// modules/blog/index.module.js
export default {
id: "blog", // unique — collision throws a build error
routes: [{ path: "/blog", entrypoint: "./routes/index.astro" }],
styles: ["./styles/blog.scss"],
hooks: {
"stnd:base": ["./components/BlogAnnouncement.astro"],
},
};
Only id is required — everything else is opt-in. Delete the folder and
the feature disappears; the app keeps running. That’s the litmus test of
a real vertical slice.
Discovery
Modules are auto-discovered from your modules/ folder (configurable via
moduleFolder in astro.config.mjs) — any file matching
**/*.module.{js,ts} is picked up, no registration step. A module that
doesn’t live in your project’s own modules/ folder — a packaged one like
@stnd/fonts/kalice, or a third-party module — is loaded explicitly via
moduleLoad instead.
Where to go from here
| Page | Covers |
|---|---|
| Building a Module | Recipes for routes, styles, config, dependencies, disabling — plus the full manifest key reference |
| Hooks & Extension Points | How modules talk to each other and to the framework’s own zones, without ever importing one another |
Reading order
Start here, then Building a Module for the day-to-day recipes. Hooks is
worth reading end-to-end before you need it — the two calling conventions
(runHook vs runPipeline) are easy to reach for backwards the first
time you need one.