@stnd/modules
The spine of the Standard application.
@stnd/modules is the discovery engine and runtime loader for the Standard vertical slice architecture. Each *.module.js manifest declares a self-contained section of your application — routes, styles, components, middleware, and actions in one folder.
The name reflects the modular, self-contained units of the application. Remove one, and that feature disappears cleanly.
ELI5
A “module” is one feature, in one folder: its routes, its styles, its components, all together. Want to add an RSS feed? moduleLoad: ["@stnd/modules/rss"] and it’s there — routes and all. Want to remove a feature entirely? Delete its folder. Nothing else in the app needs to know or change. This is the pattern behind everything: Standard itself ships as a pile of small modules (@stnd/modules/robots, /sitemap, /rss, /toast…) rather than one big framework blob.
Use a built-in module (in astro.config.mjs):
standard({ moduleLoad: ["@stnd/modules/rss", "@stnd/modules/humans"] });
Make your own: stnd module <name> (see packages/cli) scaffolds modules/<name>/index.module.js for you.
Manifest Schema
A module manifest must be named *.module.{js,ts} (convention: index.module.js) and export default { … } with this shape:
// REQUIRED
id: string // unique module id (e.g., "my-feature")
name: string // human-friendly label
description?: string // human-friendly description
// Conditional Loading
status?: "disabled" // skip this module entirely
environment?: string | string[]
// Restrict to specific Astro commands: "dev", "build", "preview"
// Accepts a single string or an array (e.g., ["dev", "preview"])
// Omit to load in all environments (default)
// Unified Hooks (Logic & Interface)
// ---------------------------------------------------------------------------
// A hook name resolves to one of two kinds — never both, for the same name:
// - LOGIC (virtual:stnd/hooks, runHook/runPipeline): the name starts with
// "astro:" (Astro's own lifecycle), OR the entry is a .js/.ts path and the
// name doesn't start with "launcher:" or end in ":action".
// - UI/action zone (virtual:stnd/components, <Hook zone="..."> + direct
// extensions[zone] reads): everything else — .astro/.svelte/.md paths,
// and any "launcher:"/":action" name regardless of file type.
//
// Two modules registering the same hook name as different kinds is a
// build-time fatal error (hook-kind-mismatch), not a silent split.
//
// These can be a single string (shorthand for `{ ui: "path" }`) or an array
// of entries (`{ ui: "..." }` / `{ action: "..." }`, or `component` for
// backward-compat with `ui`).
hooks?: {
[hookName: string]: string | Array<string | HookEntry>
}
// Routes (Astro)
routes?: Array<{
path: string // URL pattern (e.g., "/robots.txt")
entrypoint: string // relative to folio dir (e.g., "./route.js" or "./route.astro")
}>
// Styles
// - starts with "@" → imported as-is (package import)
// - else resolved relative to module dir and injected via injectScript("page-ssr")
styles?: string[]
// Scripts
// - starts with "@" → imported as-is
// - else resolved relative to module dir and injected on the client page
scripts?: string[]
// Head entries
// - string → imported like styles (SSR import)
// - { inline: string } → injected as inline head script
head?: Array<string | { inline: string }>
// Middleware
// - string → entrypoint, order defaults to 0
// - { entrypoint: string; order?: number }
middleware?: Array<string | { entrypoint: string; order?: number }>
// Astro integrations (passed through)
integrations?: Array<any>
// Actions (Astro Actions)
// - string → path to file exporting actions object(s)
actions?: string
// Content Collections
// - string → path to file exporting collections (e.g., "./content.ts")
content?: string
// Dependencies (other modules this one requires)
dependencies?: string[]
Unified Hooks Architecture
The hooks object is the brain of your module. It handles both system events and UI injection — see Hooks & Extension Points in the manual for the full walkthrough and the native hooks the framework ships with. Quick reference:
// index.module.js
export default {
id: "my-feature",
hooks: {
// UI into zones (use `ui` key, or a bare string as shorthand for it)
"stnd:base": ["./components/Banner.astro"],
"stnd:client": [{ ui: "./Drawer.svelte", meta: { "client:load": true } }],
// Lifecycle hooks and JS action handlers (use `action`)
"astro:build:done": [{ action: "./hooks/generate-feed.js" }],
"launcher:action": [{ action: "./actions/nav.js" }],
// Launcher views — a `launcher:`-prefixed name is always UI/action,
// even though its entry is registered under `ui`
"launcher:view": [{ ui: "./views/ShareView.svelte", trigger: "::share", meta: { title: "Share" } }],
},
};
Consuming Hooks
UI Rendering (Zones):
In your Layout or components, use the <Hook /> component to render every registered entry for a zone name — the prop is zone, not id.
---
import Hook from "@stnd/core/Hook";
---
<Hook zone="stnd:base" />
<Hook zone="stnd:client" hydrated props={{ theme: "dark" }} />
Logic Execution:
Trigger logic hooks via the virtual module — runHook fans out and collects every module’s result; runPipeline threads one value through each handler in sequence.
import { runHook } from "virtual:stnd/hooks";
const results = await runHook("astro:build:done", buildContext);
Middleware
Module middlewares are native Astro Middlewares. They must follow the (context, next) signature and call next() to continue the chain.
// index.module.js
export default {
id: "auth",
middleware: [{ entrypoint: "./middleware.js", order: -100 }],
};
// middleware.js
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
// Root initialization, auth checks, etc.
return next();
});
Content Extensions
Modules can define Astro Content Collections.
// index.module.js
export default {
id: "my-feature",
content: "./content.ts",
};
// content.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
export const myCollection = defineCollection({
loader: glob({ pattern: "*.md", base: "./content/my-collection" }),
schema: z.object({
/* ... */
}),
});
The application’s src/content.config.ts imports and merges these collections:
import { collections as moduleCollections } from "virtual:stnd/content";
export const collections = {
...moduleCollections,
};
Authoring Guide
- Place modules under
modules/<name>/index.module.jsat the project root. - Keep logic inside the module;
.astrofiles should only consume model instances. - Import from sibling modules via
@modules/<name>— this alias is auto-registered by@stnd/core. - Prefer OKLCH and Standard tokens for styles; avoid one-off CSS.
- No backward compatibility — ship only the current shape.
The @modules Import Alias
@stnd/core automatically registers @modules as a Vite alias pointing to the app’s modules/ directory. Every app gets this for free — no manual tsconfig or Vite config needed.
import { Note } from "@modules/spine/models/Note";
import Author from "@modules/spine/models/Author";
import Base from "@modules/base/layouts/Base.astro";
The corresponding tsconfig.json path (for editor intellisense):
{
"compilerOptions": {
"paths": {
"@modules/*": ["modules/*"]
}
}
}
Boundary Rules
Modules follow strict vertical slice isolation enforced by dependency-cruiser:
- Foundation modules (
models,core) — importable by any module - Feature modules (everything else) — must NOT import from sibling feature modules
- One-way dependencies — features → foundation →
@stnd/*packages, never reversed - No circular deps — within or across modules
Run the boundary check:
pnpm boundaries:gd # Check Standard Garden
pnpm boundaries:ade # Check L'art d'enseigner
Loader Behavior
- Discovers
**/*.module.{js,ts}in the configuredmoduleFolder(default:modules). moduleLoadinastro.configaccepts:- Bare names (auto-prefixed):
"launcher","design", etc. - Explicit specifiers:
"@stnd/modules/design"or"./local/feature".
- Bare names (auto-prefixed):
- Routes, styles, scripts, head, middleware are injected per manifest.
- Integrations are forwarded to Astro via
updateConfig. - UI/Component extensions are exposed via
virtual:stnd/components. - Client payload strips infrastructure keys; keeps
__importPathfor server use.
Disabling a Module
Prefix the folder name with _ to temporarily disable without deleting:
mv modules/export/ modules/_export/ # Disabled
mv modules/_export/ modules/export/ # Re-enabled
The loader skips any folders starting with _.
Environment-Gated Modules
Restrict a module to specific Astro commands (dev, build, or preview) using the environment field. The module is skipped entirely when the current command doesn’t match.
// Only loaded during `astro dev`
export default {
id: "dev-tools",
name: "Dev Tools",
environment: "dev",
};
// Loaded during `astro dev` and `astro preview`, but not `astro build`
export default {
id: "staging-tools",
name: "Staging Tools",
environment: ["dev", "preview"],
};
Omit the field to load in all environments (the default). When a module is skipped, its routes, styles, scripts, middleware, and hooks are completely absent from the build — as if the module didn’t exist.
Shipped Modules
These built-in modules come with @stnd/modules and can be loaded via moduleLoad:
Gold Standard (loaded by default)
Every @stnd site ships with these — the definitive list lives in
GOLD_STANDARD_MODULES in packages/core/standard.js. Opt out via
moduleExclude. (Three more gold standard entries — @stnd/fonts/inter,
@stnd/fonts/source-serif-4, @stnd/fonts/ibm-plex — and @stnd/icon/module
ship from their own packages, not from here.)
| Module | ID | Route | What it does |
|---|---|---|---|
@stnd/modules/toast |
stnd-toast |
— | Zero-dependency global notification system |
@stnd/modules/confetti |
stnd-confetti |
— | A fun confetti explosion on page load |
@stnd/modules/lab |
stnd-lab |
— | Loads the StandardLab inspector/debug bundle (dev mode) |
@stnd/modules/launcher |
stnd::launcher |
— | Universal command palette engine |
@stnd/modules/styles |
stnd-styles |
— | Injects the Standard design stylesheet |
@stnd/modules/copy-buttons |
stnd-copy-buttons |
— | Adds copy-to-clipboard buttons to code blocks |
@stnd/modules/image-zoom |
stnd-image-zoom |
— | Click-to-zoom lightbox behavior for images |
@stnd/modules/scroll-wrappers |
stnd-scroll-wrappers |
— | Scroll-linked wrapper behaviors for content |
@stnd/modules/mermaid |
mermaid |
— | Diagram and flowchart rendering with Mermaid.js |
@stnd/modules/math |
math |
— | Mathematical notation rendering with KaTeX |
@stnd/modules/prism |
stnd-prism |
— | Syntax highlighting via Prism.js, loaded from CDN |
@stnd/modules/robots |
stnd-robots |
/robots.txt |
Generates robots.txt from site config |
@stnd/modules/headers |
stnd-headers |
/_headers |
Emits security headers (HSTS, X-Frame-Options, Permissions-Policy) |
@stnd/modules/manifest |
stnd-manifest |
/site.webmanifest |
Serves the web app manifest |
@stnd/modules/sitemap |
stnd-sitemap |
— | Sitemap generation via @astrojs/sitemap |
Opt-In Modules
Load these explicitly via moduleLoad when your site needs them.
| Module | ID | Route | What it does |
|---|---|---|---|
@stnd/modules/rss |
stnd-rss |
/rss.xml |
Generates an RSS 2.0 feed from site content and config |
@stnd/modules/security-txt |
stnd-security-txt |
/.well-known/security.txt |
RFC 9116 security contact disclosure |
@stnd/modules/humans |
stnd-humans |
/humans.txt |
The people and tools behind the site |
@stnd/modules/themes |
stnd-themes |
— | Theme/temperament stylesheet injection |
@stnd/modules/content |
stnd-content |
/[…slug] |
Content collection catch-all route |
@stnd/modules/maintenance |
stnd-maintenance |
/maintenance |
Maintenance mode with redirect middleware |
@stnd/modules/brand-manual |
stnd-brand-manual |
/brand |
A brand manual page showing the active theme’s design tokens |
@stnd/modules/deep-link |
stnd-deep-link |
— | Deep-linking client behavior |
@stnd/modules/eink |
stnd-eink |
— | E-ink display detection and adaptation |
@stnd/modules/fonts |
stnd-fonts |
— | Loads every shipped font folio at once, instead of cherry-picking one |
@stnd/modules/gestures |
stnd-gestures |
— | Touch/gesture client behaviors |
@stnd/modules/gsap |
gsap |
— | GSAP animation library with ScrollTrigger, loaded from CDN on demand |
@stnd/modules/iconify |
stnd-iconify |
— | Client-side icon resolution via Iconify |
@stnd/modules/keyboard |
stnd-keyboard |
— | Keyboard-shortcut client behaviors |
@stnd/modules/p5 |
p5 |
— | Creative coding with p5.js, preloaded from CDN as window.p5 |
@stnd/modules/stripe |
stnd-stripe |
/api/stripe/mock-checkout |
Commerce integration and mock checkout service |
@stnd/modules/theme-utils |
stnd-theme-utils |
— | Global theme switcher — apply themes via data-theme buttons |
Usage in an App
Gold standard modules load automatically — just add your fonts, themes, and features:
// astro.config.mjs
import standard from "@stnd/core";
export default defineConfig({
integrations: [
standard({
// Gold standard modules load automatically:
// styles, robots, headers, manifest, sitemap, @stnd/fonts/inter
// Add your own modules on top of the defaults
moduleLoad: [
"@stnd/modules/rss",
"@stnd/modules/humans",
"@stnd/modules/security-txt",
"@stnd/fonts/kalice",
"@stnd/themes/editorial",
],
}),
],
});
To opt out of a specific default, use moduleExclude:
standard({
// Everything except the sitemap
moduleExclude: ["@stnd/modules/sitemap"],
moduleLoad: ["@stnd/modules/rss"],
});
Philosophy
- Vertical slice: each module is self-contained — a section of the application.
- Strict boundaries: features don’t cross-import. Dependencies flow one way.
- Zero shims: no legacy flags, no backward compatibility layers.
- Performance and clarity: small, explicit manifests; no hidden magic.
Notes / Observations
(jot down anything noticed here — quirks, gotchas, ideas)
Todo
- Nothing tracked yet. priority: 3 token_scale: 3 created: 2026-07-14 area: framework
“A well-bound app holds together not because of glue, but because every module knows its place.”