@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.
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)
// —————————————————————————
// Standard automatically routes hooks based on their file extension:
// – .js, .ts → LOGIC (Listeners / Handlers)
// – .astro, .svelte, .md → UI (Components / Plugs)
//
// These can be a single string or an array of entries.
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.
1. Integration Hooks (Logic)
If the hook name starts with astro: or the entry ends in .js/.ts, it’s treated as logic.
// index.module.js
export default {
id: “my-feature”,
hooks: {
“astro:config:setup”: “./hooks/setup.js”, // Astro native hook
“app:init”: “./hooks/init.ts”, // Custom app hook
},
};
2. Interface Hooks (UI Plugs)
If the entry ends in .astro, .svelte, .md, or any other format, it’s treated as a UI component.
// index.module.js
export default {
id: “my-feature”,
hooks: {
“header:top”: [“./components/Banner.astro”],
“footer:bottom”: “./components/Copyright.astro”,
},
};
Consuming Hooks
UI Rendering (Zones):
In your Layout or components, use the <Hook /> component to render all registered components for a hook ID.
—
import Hook from “@stnd/core/Hook.astro”;
—
<header>
<Hook id=“header:top” props={{ theme: “dark” }} />
</header>
Logic Execution:
Trigger logic hooks via the virtual module.
import { runHook } from “virtual:standard/hooks”;
await runHook(“app:init”, { some: “data” });
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:standard/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:standard/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. Opt out via moduleExclude.
| Module | ID | Route | What it does |
|---|---|---|---|
@stnd/modules/styles |
stnd-styles |
— | Injects the Standard design stylesheet |
@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/lab |
stnd-lab |
— | StandardLab CSS inspector (dev tool) |
@stnd/modules/content |
stnd-content |
/[…slug] |
Content collection catch-all route |
@stnd/modules/maintenance |
stnd-maintenance |
/maintenance |
Maintenance mode with redirect middleware |
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.
“A well-bound app holds together not because of glue, but because every module knows its place.”