Module System
Modules are the building blocks of a Standard OS site. Each one is a self-contained vertical slice — it can add routes, styles, middleware, config, nav items, UI components, and more. Drop it in, it wires itself up.
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.
Anatomy 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
name: “Blog”,
description: “Posts, tags, RSS.”,
status: “enabled”, // or “disabled” to skip without deleting
dependencies: [“stnd-styles”], // loaded before this module
config: {
title: “My Blog”,
nav: {
header: [{ title: “Blog”, url: “/blog” }],
},
},
routes: [
{ path: “/blog”, entrypoint: “./routes/index.astro” },
{ path: “/blog/[slug]”, entrypoint: “./routes/post.astro” },
],
styles: [“./styles/blog.scss”],
scripts: [“./client.js”],
actions: “./actions.ts”,
hooks: {
“stnd:base”: [“./components/BlogAnnouncement.astro”],
},
};
Only id is required. Everything else is opt-in.
Module 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 manual registration needed.
modules/
blog/
index.module.js ← discovered automatically
shop/
index.module.js ← discovered automatically
You can also load external modules explicitly via moduleLoad in your Astro config:
// astro.config.mjs
standard({
moduleLoad: [“@stnd/fonts/kalice”, “my-custom-package/module”],
});
Explicit modules always load before discovered ones. Gold standard modules (toast, launcher, styles, sitemap, etc.) are always loaded first.
Load Order
Modules load in a deterministic order:
- Gold standard modules — internal system modules, in their declared order
moduleLoadentries — your explicit additions, in the order you wrote them- Discovered modules — auto-found in
modules/, alphabetical - Dependencies — pulled in before the module that requires them
The topological sort (Kahn’s algorithm) guarantees a dependency always precedes its consumer. Within a tier, alphabetical order is the stable tiebreaker.
Config Contributions
Modules can contribute to the global site config via the config key. These are deep-merged in load order:
// modules/seo/index.module.js
export default {
id: “seo”,
config: {
nav: {
footer: [{ title: “Privacy”, url: “/privacy” }],
},
},
};
Arrays (like nav.header) are concatenated across modules — each module appends its items. Objects are recursed. Scalars are protected: two modules claiming the same scalar key is a build error.
Config Conflict: Key “title” was claimed by both “core” and “seo”.
Only one module may define a scalar config value.
Use arrays or nested objects for multi-module contributions.
Your top-level astro.config.mjs options always win last, silently — they’re the final override.
The merged config is available everywhere via virtual:stnd/config:
import config from “virtual:stnd/config”;
console.log(config.nav.header); // all modules’ nav items, combined
Dependencies
Declare what your module needs. The loader guarantees it’s ready before yours runs:
export default {
id: “shop”,
dependencies: [“stnd-styles”, “core”, “@stnd/fonts/kalice”],
// …
};
Unresolved dependencies log a warning but don’t crash. Circular dependencies throw immediately.
Disabling a Module
Set status: “disabled” to skip a module without removing it:
export default {
id: “analytics”,
status: “disabled”, // loaded but not processed
// …
};
Or exclude a gold standard module entirely from your Astro config:
standard({
moduleExclude: [“@stnd/launcher”],
});
Runtime Store
Modules can provide values to the runtime store (a simple Map-based provide/inject):
// in the module manifest
store: {
user: “./models/Visitor.js#visitorStore”, // named export
search: “./search.js”, // default export
}
// in any component
import { inject } from “@stnd/store”;
const user = inject(“user”);
Values are initialized before any component mounts.
Hooks & UI Zones
Modules can render components into named zones across the layout:
hooks: {
“stnd:base”: [“./components/Toast.astro”],
“stnd:client”: [{ ui: “./Drawer.svelte”, meta: { “client:load”: true } }],
}
Zones are rendered with <Hook name=“stnd:base” /> anywhere in your layouts. Multiple modules can contribute to the same zone — their components are rendered in load order.
Action hooks (.js/.ts entries) run at build lifecycle points:
hooks: {
“astro:build:done”: [{ action: “./hooks/generate-feed.js” }]
}
All Module Keys
| Key | Type | Description |
|---|---|---|
id |
string |
Required. Unique identifier. Duplicate IDs throw. |
name |
string |
Display name for logs and dev tools. |
description |
string |
Short description. |
status |
“enabled” | “disabled” |
Skip processing when “disabled”. |
dependencies |
string[] |
Module IDs that must load first. |
config |
object |
Merged into global config. Scalar conflicts throw. |
routes |
{ path, entrypoint }[] |
Astro routes to inject. |
styles |
string[] |
CSS/SCSS files, injected on every page. |
scripts |
string[] |
JS files or CDN URLs, injected on every page. |
middleware |
string[] | { entrypoint, order }[] |
Request handlers. order controls sequence. |
actions |
string |
Path to Astro Actions file. |
hooks |
{ [hookName]: entry[] } |
UI zones and lifecycle hooks. |
store |
{ [key]: “path#export” } |
Runtime provide/inject registrations. |
aliases |
{ [alias]: path } |
Vite path aliases. |
content |
string |
Path to content collections definition. |