Building a Module
Add a route
routes: [
{ path: "/blog", entrypoint: "./routes/index.astro" },
{ path: "/blog/[slug]", entrypoint: "./routes/post.astro" },
],
Add global styles or scripts
styles: ["./styles/blog.scss"], // injected on every page
scripts: ["./client.js"], // JS files or CDN URLs
Contribute to the site’s nav, title, or any other config
Modules deep-merge into the global config via the config key:
// 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 result is available everywhere via
virtual:stnd/config:
import config from "virtual:stnd/config";
console.log(config.nav.header); // all modules' nav items, combined
Declare a dependency
export default {
id: "shop",
dependencies: ["stnd-styles", "core", "@stnd/fonts/kalice"],
// ...
};
The loader guarantees a dependency is ready before your module runs — a
topological sort (Kahn’s algorithm) does the ordering, so you never have
to hand-tune load order yourself. Unresolved dependencies log a warning
but don’t crash; circular dependencies throw immediately.
Provide a value to the runtime store
For state other modules or components need without importing you
directly (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.
Disable a module without deleting 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"],
});
If you need to know the load 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
Explicit modules (moduleLoad) always load before auto-discovered ones.
Within a tier, alphabetical order is the stable tiebreaker.
Quick reference — every manifest key
| 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 — see Hooks & Extension Points. |
store |
{ [key]: "path#export" } |
Runtime provide/inject registrations. |
aliases |
{ [alias]: path } |
Vite path aliases. |
content |
string |
Path to content collections definition. |