stnd.buildSTANDARD MANUAL2026-09-20

@stnd/modules

@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

“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

  1. Place modules under modules/<name>/index.module.js at the project root.
  2. Keep logic inside the module; .astro files should only consume model instances.
  3. Import from sibling modules via @modules/<name> — this alias is auto-registered by @stnd/core.
  4. Prefer OKLCH and Standard tokens for styles; avoid one-off CSS.
  5. 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 configured moduleFolder (default: modules).
  • moduleLoad in astro.config accepts:
    • Bare names (auto-prefixed): "launcher", "design", etc.
    • Explicit specifiers: "@stnd/modules/design" or "./local/feature".
  • 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 __importPath for 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.”