Module System
Vertical slices. One manifest. Delete the folder, the feature disappears.
The app keeps running. That's the litmus test.
How it works
Discover
Standard scans modules/ for *.module.js files and
loads Gold Standard packages from @stnd/*.
Sort
Dependencies are resolved into a topological order. Circular dependencies throw immediately.
Wire
Routes, styles, scripts, middleware, hooks, and UI zones are injected into the Astro build pipeline.
Anatomy of a module
Every module is a folder with an index.module.js manifest. The manifest
declares what the module contributes — routes, styles, hooks, UI — and the framework
wires it all together.
modules/
└── my-feature/
├── index.module.js ← The manifest (start here)
├── routes/
│ └── index.astro ← Pages
├── models/
│ └── Thing.ts ← Domain logic
├── ui/
│ └── Widget.svelte ← Components
├── actions.ts ← Server actions
├── middleware.ts ← Request pipeline
└── styles/
└── feature.scss ← Scoped styles Minimal example
The manifest
// modules/blog/index.module.js
export default {
id: "blog",
name: "Blog",
description: "A simple blog.",
routes: [
{ path: "/blog", entrypoint: "./routes/index.astro" },
{ path: "/blog/[slug]", entrypoint: "./routes/[slug].astro" },
],
styles: ["./styles/blog.scss"],
}; That's it
# What you get:
* /blog route registered
* /blog/[slug] route registered
* blog.scss injected globally
* @modules/blog alias available
* Hot reload when manifest changes
# Delete the folder:
* Routes disappear
* Styles disappear
* App keeps running The hooks API
The hooks key is the single unified API for all module contributions
beyond routes and styles. It handles UI zones, lifecycle events, and action handlers
— all in one place.
export default {
id: "my-feature",
name: "My Feature",
hooks: {
// ── UI zones ──────────────────────────────────────
// Inject components into the page layout.
// Use "ui" for the component path.
// Plain strings are shorthand for { ui: "path" }.
"stnd:base": ["./MyNotification.astro"],
// ↑ shorthand — no meta needed
"stnd:client": [
{
ui: "./MyWidget.svelte",
meta: { "client:load": true },
},
],
// ── Lifecycle hooks ───────────────────────────────
// Run code at build stages. Points to JS/TS files.
"astro:build:done": [{ action: "./on-build-done.js" }],
// ── Custom hooks ──────────────────────────────────
// Launcher views, actions, or any custom system.
"launcher:view": [
{
ui: "./views/MyView.svelte",
trigger: "::myview",
meta: { title: "My View", icon: "ph:star" },
},
],
"launcher:action": [
{ action: "./actions/my-action.js", meta: { label: "Do Thing" } },
],
},
}; Resolution rules
| Hook name | Entry type | Result |
|---|---|---|
astro:* | Any | Astro lifecycle hook |
| Anything else | .js / .ts | Runtime action handler |
| Anything else | .astro / .svelte | UI zone contribution |
| Anything else | Plain string | UI zone contribution (shorthand) |
Hook zones
Zones are named injection points in the layout where modules contribute UI. The framework Base layout renders two built-in zones. Apps can define more.
stnd:base
Static Astro components rendered at build time. No JavaScript overhead. Perfect for HTML + inline scripts.
- Toast notification container
- Confetti celebration script
// Rendered in Base.astro as:
<Hook zone="stnd:base" /> stnd:client
Hydrated Svelte components with client:load. For anything
that needs client-side interactivity.
- Lab (CSS token inspector)
- DropZone (drag & drop uploads)
- Graft Toolbar (text selection actions)
// Rendered in Base.astro as:
<Hook zone="stnd:client" hydrated={true} /> Custom zones
Apps can define their own zones. Just render a <Hook> in
your layout and contribute to it from any module.
In your layout
---
import Hook from "@stnd/core/Hook";
---
<aside>
<Hook zone="sidebar" />
</aside> In any module
// modules/weather/index.module.js
export default {
id: "weather",
name: "Weather Widget",
hooks: {
"sidebar": ["./WeatherCard.astro"],
},
}; The system layer
Every page rendered with Base.astro includes a system layer that
persists across navigations. Gold Standard modules inject their UI here automatically
— you never import Toast, Confetti, or Lab manually.
<!-- Inside @stnd/layout/Base.astro -->
<div id="standard-os" transition:persist="standard-os">
<!-- Zero-JS: Toast container, Confetti script -->
<Hook zone="stnd:base" />
<!-- Hydrated: Lab inspector, app extensions -->
<Hook zone="stnd:client" hydrated={true} />
</div> Gold Standard modules
Every @stnd/core site ships with these modules by default. No configuration
needed — they're loaded automatically. Opt out of any module with
moduleExclude.
System UI
Toast Notifications
Global notification system. Provides window.toast() on every page.
Confetti
Page-load celebration effect via canvas-confetti CDN.
Lab
Live CSS token inspector and design debug tools.
Launcher
Universal command palette engine — views, actions, and keyboard navigation.
Styles & Fonts
Styles
Golden ratio typography, Swiss grid, OKLCH color system.
Inter
Self-hosted Inter typeface.
Source Serif 4
Self-hosted Source Serif 4 typeface.
IBM Plex Mono
Self-hosted IBM Plex Mono typeface.
Client Enhancements
SEO & Web Standards
Copy Buttons
Automatic copy-to-clipboard buttons on code blocks.
Image Zoom
Click-to-zoom on images for detail inspection.
Scroll Wrappers
Horizontal scroll containers for wide tables and code.
Robots
Auto-generated robots.txt for search engine crawling.
Headers
Security and caching HTTP headers.
Manifest
Web app manifest for PWA installability.
Sitemap
Auto-generated sitemap.xml for search engines.
Opt out
Don't want confetti? Exclude it in your Astro config. The module is skipped entirely — no code, no styles, no scripts.
// astro.config.mjs
import standard from "@stnd/core";
export default defineConfig({
integrations: [
standard({
moduleExclude: [
"@stnd/modules/confetti", // No celebration
"@stnd/modules/lab", // No debug inspector
],
}),
],
}); Add your own modules
Load additional @stnd/* modules or any npm package that exports
a module manifest.
// astro.config.mjs
import standard from "@stnd/core";
export default defineConfig({
integrations: [
standard({
moduleLoad: [
"@stnd/modules/rss", // Add RSS feed
"@stnd/modules/humans", // Add humans.txt
"@stnd/fonts/cargo-diatype", // Add a typeface
],
}),
],
});
App-level modules are discovered automatically from your
modules/ folder — no registration needed.
Manifest reference
Every key a module manifest can declare. All keys are optional except id and name.
| Key | Type | Description |
|---|---|---|
| id * | string | Unique module identifier. |
| name * | string | Human-readable display name. |
| description | string | What this module does. |
| status | "enabled" | "disabled" | Set to "disabled" to skip loading. |
| dependencies | string[] | Module IDs this module depends on. |
| routes | array | Astro routes to inject. |
| styles | string[] | CSS/SCSS files to inject globally. |
| scripts | string[] | Client scripts to inject (paths or CDN URLs). |
| middleware | array | Server middleware to register. |
| actions | string | Path to Astro actions file. |
| hooks | object | UI zones and lifecycle hooks. |
| aliases | object | Vite path aliases (e.g. @myfeature → ./src). |
| content | string | Content collection configuration. |
| config | object | Contribute to the global site config (nav, title, etc.). |
Hook entry formats
Each hook zone accepts an array of entries. Entries can be strings
(shorthand) or objects with ui, action,
meta, and trigger.
hooks: {
"stnd:base": [
// String shorthand — just a path, no metadata
"./MyComponent.astro",
],
"stnd:client": [
// Object — full control
{
ui: "./MyWidget.svelte", // The component to render
meta: { "client:load": true }, // Props / hydration directives
},
],
"launcher:view": [
// Launcher view — ui + trigger + meta
{
ui: "./views/SearchView.svelte",
trigger: "::search",
meta: {
title: "Search",
icon: "ph:magnifying-glass",
size: { width: "standard", height: "standard" },
},
},
],
"launcher:action": [
// Action handler — points to a JS file
{
action: "./actions/navigation.js",
meta: { label: "Navigation" },
},
],
} Full example
A complete module manifest using every available key.
// modules/upload-system/index.module.js
export default {
id: "upload-system",
name: "Upload System",
description: "Global drag and drop zone for file uploads.",
dependencies: ["root"],
routes: [
{ path: "/uploads", entrypoint: "./routes/index.astro" },
],
styles: ["./styles/dropzone.scss"],
scripts: ["./client/upload-progress.js"],
middleware: [
{ entrypoint: "./middleware.ts", order: 10 },
],
actions: "./actions.ts",
hooks: {
"stnd:client": [
{
ui: "./GlobalDropZone.svelte",
meta: { "client:load": true },
},
],
},
config: {
maxUploadSize: "10MB",
},
aliases: {
"@uploads": ".",
},
}; Module lifecycle
Modules are loaded once during astro:config:setup and their contributions
are wired into the build pipeline.
- Discover — Scans
modules/for*.module.jsfiles and resolvesmoduleLoadpackages. - Import — Dynamic-imports each manifest. Validates that every
module has a unique
id. - Sort deps — Builds a dependency graph and produces a topological load order. Circular dependencies throw.
- Wire — Routes are injected via
injectRoute. Styles and scripts become virtual modules. Middleware is registered. Hook zones are populated. Aliases are set. - Build — Astro takes over. Virtual modules are resolved. Components render. Everything is bundled.
Import rules
Modules follow strict import boundaries. This keeps the architecture clean and ensures any module can be deleted without breaking others.
| From | Can import | Cannot import |
|---|---|---|
| Any module | @stnd/* packages | Other app modules |
| Any module | @modules/core/* (the foundation) | — |
| Feature module | Its own files | Another feature module |
| Core module | Its own files | Feature modules |
// ✓ Good — importing from @stnd packages
import Icon from "@stnd/icon/Icon.svelte";
import { slugify } from "@stnd/utils";
// ✓ Good — importing from the core module
import Note from "@modules/core/models/Note";
// ✗ Bad — cross-importing between feature modules
import Thing from "@modules/upload-system/Thing"; // NO Quick reference
| I want to… | Manifest key |
|---|---|
| Add a page | routes: [{ path, entrypoint }] |
| Add global CSS | styles: ["./my.scss"] |
| Add a client script | scripts: ["./my.js"] |
| Add server middleware | middleware: [{ entrypoint, order }] |
| Add server actions | actions: "./actions.ts" |
| Inject UI into the base layout | hooks: { "stnd:base": ["./My.astro"] } |
| Inject a hydrated Svelte component | hooks: { "stnd:client": [{ ui, meta }] } |
| Add a Launcher view | hooks: { "launcher:view": [{ ui, trigger, meta }] } |
| Add a Launcher action | hooks: { "launcher:action": [{ action, meta }] } |
| Set a Vite alias | aliases: { "@mine": "." } |
| Contribute to site config | config: { nav: [...] } |
| Depend on another module | dependencies: ["other-module-id"] |
| Disable a module | status: "disabled" |