stnd.build · DocumentationSTANDARD MANUALSTD-STND-MOD · 2026-03-15
STD-STND-MOD

@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.

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

  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:standard/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. 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.”

Standard OS — stnd.buildSTD-STND-MOD · rev. 2026-03-15