stnd.build · DocumentationSTANDARD MANUALSTD-MODULE-S · 2026-03-15
STD-MODULE-S

Module System

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:

  1. Gold standard modules — internal system modules, in their declared order
  2. moduleLoad entries — your explicit additions, in the order you wrote them
  3. Discovered modules — auto-found in modules/, alphabetical
  4. 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.
Standard OS — stnd.buildSTD-MODULE-S · rev. 2026-03-15