Welcome to Stackwright! This is a YAML-driven React application framework that enables rapid development of professional websites and applications through a "content as code" approach. In this guide, you'll find essential knowledge required to be productive in the Stackwright project. For contributor guidelines (branching, commits, testing, changesets), see CONTRIBUTING.md.
The fastest way to get started with Stackwright is using launch-stackwright:
Recommended: Full otter raft experience (auto-installs dependencies)
npx launch-stackwright my-site --otter-raft
cd my-site
pnpm devAlternative: Manual setup
npx launch-stackwright my-site
cd my-site
pnpm install
pnpm devBoth set up:
- ✅ A fully configured Next.js + Stackwright project
- ✅ The otter raft (AI agents) ready to build your site
- ✅ MCP server auto-configuration for Code Puppy
See the Otter Raft documentation for how to use the AI agents to build complete sites through conversation.
- Framework Architecture: To understand the big picture, read:
packages/core/src/index.ts: Core framework initializationpackages/nextjs/src/components/StackwrightLayout.tsx: App Router root layout (App Router) orStackwrightDocument.tsx(Pages Router, deprecated)packages/themes/src/ThemesProvider.tsx: Theme provider for theme handling
- Developer Workflows
- Build: Run
pnpm buildfrom the project root - Test: Run
pnpm testfrom the project root (runs vitest across all packages)
- Build: Run
- Project Conventions: Note these important patterns that differ from common practices
- File Organization
- All source code is in
packagesdirectory - Core framework components are in
src/componentsof@stackwright/corepackage - Themes are defined in YAML files within the
themesdirectory of the same package
- All source code is in
- Naming Conventions
- Kebab-case for file names (e.g.,
main-content-grid.tsx) - PascalCase for components (e.g.,
DynamicPage)
- Kebab-case for file names (e.g.,
- File Organization
The stackwrightRegistry is a singleton that must be populated before rendering. In Next.js apps:
- Call
registerNextJSComponents()from@stackwright/nextjsinapp/layout.tsx(App Router — via a'use client'Providers component; see@stackwright/nextjsAGENTS.md for the correct pattern) orpages/_app.tsx(Pages Router — deprecated) - Call
registerDefaultIcons()from@stackwright/iconsin the same location - Do not rely on module import side effects for registration — it must be explicit
createStackwrightNextConfig() from @stackwright/nextjs should be used in next.config.js instead of manual webpack configuration.
- Each package uses tsup for dual-format output (ESM
.mjs+ CJS.js) - Do NOT add
"type": "module"to anypackages/*package.json. tsup's.mjs/.jsextension convention handles format signaling. Adding"type": "module"breaksrequire()calls in Next.js config files.
Images can be placed alongside their page YAML files in content/pages/. Use ./relative paths in YAML (e.g. src: ./hero.png). These are processed by the stackwright-prebuild script (from @stackwright/build-scripts) which runs before next build and next dev:
- Image is copied to
public/images/preserving directory structure - Path is rewritten to
/images/...in the processed JSON getStaticPropsreads from the processed JSON — nofswork at render time
Required: Add these hooks to the Next.js app's package.json:
"prebuild": "stackwright-prebuild",
"predev": "stackwright-prebuild"Without these hooks, co-located images will not be found at runtime.
Stackwright supports multi-locale sites via locale-suffix content files and locale-aware prebuild output.
Enable i18n in stackwright.yml:
locales:
default: en
supported:
- en
- fr
- dePage content files — full replacement, not merge:
pages/
about/
content.yml # default locale (en)
content.fr.yml # French — entirely replaces content.yml for /fr/about
content.de.yml # German
content.<locale>.ymlmust be a complete, valid page YAML — it is NOT merged withcontent.yml- A locale file can be omitted for any page; the default locale is served silently as fallback
Locale site configs (nav, appBar, footer in another language):
- Place
stackwright.fr.ymlalongsidestackwright.ymlin the project root - The prebuild outputs
public/stackwright-content/_site.fr.jsonautomatically - Only fields that differ need overriding — but the file must still be a complete valid
siteConfigSchemadoc
Prebuild output structure:
public/stackwright-content/
_site.json # default locale site config
_site.fr.json # French site config (from stackwright.fr.yml)
_root.json # default locale root page
about.json # default locale /about
fr/
_root.json # French root page
about.json # French /fr/about
URL structure: /fr/about serves French content; /about serves the default locale.
Fallback: If content.fr.yml does not exist for a page, getStackwrightPageData falls back to content.yml silently — no 404, no warning.
Static params: generateStackwrightStaticParams() recursively walks the content dir and returns both { slug: ['about'] } and { slug: ['fr', 'about'] } automatically.
Helper utilities (from @stackwright/nextjs):
getStackwrightSiteLocales()— readslocales.supportedfrom_site.json; defaults to['en']parseLocaleFromSlug(slug, supportedLocales)— strips locale prefix fromparams.slug:['fr', 'about']→{ locale: 'fr', pageSlug: ['about'] }['about']→{ locale: 'en', pageSlug: ['about'] }(default locale)
getStackwrightPageData(pageSlug, locale)— reads the locale-specific JSON, falls back silently
MCP tool updates:
stackwright_write_page— accepts optionallocaleparam; writescontent.<locale>.yml; full schema validation appliedstackwright_get_page— accepts optionallocaleparam; falls back to default with a note if locale file absentstackwright_list_pages— shows available locales per page:/about — About Us [en, fr]
AGENTS: This table is auto-generated from the live Zod schemas. Run pnpm stackwright -- generate-agent-docs to regenerate. Do NOT edit the content between the markers manually.
The YAML key is the key used inside content_items entries. All types inherit label (required), color (optional), and background (optional) from BaseContent.
| YAML key | Required fields | Optional fields |
|---|---|---|
carousel |
label (string), heading (string), items (CarouselItem[]) |
color (string), background (string), autoPlaySpeed (number), infinite (boolean), autoPlay (boolean) |
main |
label (string), heading (TextBlock), textBlocks (TextBlock[]) |
color (string), background (string), media (MediaItem), graphic_position (left |
tabbed_content |
label (string), heading (TextBlock), tabs (object |
object |
media |
label (string), src (string) |
color (string), background (string), alt (string), height (number |
timeline |
label (string), items (TimelineItem[]) |
color (string), background (string), heading (TextBlock), layout (vertical |
icon_grid |
label (string), icons (IconContent[]) |
color (string), background (string), heading (TextBlock) |
code_block |
label (string), code (string) |
color (string), background (string), language (string), lineNumbers (boolean) |
feature_list |
label (string), items (object[]) |
color (string), background (string), heading (TextBlock), columns (number) |
testimonial_grid |
label (string), items (object[]) |
color (string), background (string), heading (TextBlock), columns (number) |
faq |
label (string), items (object[]) |
color (string), background (string), heading (TextBlock) |
pricing_table |
label (string), plans (object[]) |
color (string), background (string), heading (TextBlock) |
alert |
label (string), variant (info |
warning |
contact_form_stub |
label (string), email (string) |
color (string), background (string), heading (TextBlock), description (string), email_subject (string), phone (string), address (string), button_text (string) |
form |
label (string), fields (object[]), action (string) |
color (string), background (string), heading (TextBlock), description (string), method (GET |
text_block |
label (string), textBlocks (TextBlock[]) |
color (string), background (string), heading (TextBlock), buttons (ButtonContent[]) |
grid |
label (string), columns (GridColumn[]) |
color (string), background (string), heading (TextBlock), gap (string), stackBelow (number) |
collection_list |
label (string), source (string), layout (default), card (object) |
columns (number), limit (number), hrefPrefix (string), heading (TextBlock), background (string), color (string) |
video |
label (string), src (string) |
color (string), background (string), alt (string), height (number |
map |
label (string), center (object), zoom (number) |
color (string), background (string), markers (object[]), layers (object[]), view (map |
Sub-type reference:
| Type | Fields |
|---|---|
TextBlock |
text (string), textSize (TypographyVariant), textColor? (string) |
ButtonContent |
text (string), textSize (TypographyVariant), textColor? (string), variant (text |
MediaItem |
Discriminated union: type: "media" | type: "icon" | type: "image" | type: "video". type field is required and acts as discriminator. |
ImageContent |
label (string), color? (string), background? (string), src (string), alt? (string), height? (number |
IconContent |
label (string), color? (string), background? (string), src (string), alt? (string), height? (number |
CarouselItem |
title (string), text (string), media (MediaItem), background? (string) |
TimelineItem |
year (string), event (string), yearColor? (string), cardBackground? (string), dotColor? (string) |
GridColumn |
width? (number), content_items (object |
TypographyVariant values: h1 h2 h3 h4 h5 h6 subtitle1 subtitle2 body1 body2 caption button overline
AGENTS: This table is auto-generated from @stackwright/types. Run pnpm stackwright -- generate-agent-docs to regenerate. Do NOT edit the content between the markers manually.
All interface contracts are defined in @stackwright/types and re-exported from @stackwright/collections, @stackwright/hooks-registry, and @stackwright/scaffold-core for backward compatibility.
| Interface / Type | Kind | Fields / Signature |
|---|---|---|
CollectionProvider |
interface | list(collection, opts?) (Promise), get(collection, slug) (Promise<CollectionEntry |
CollectionEntry |
interface | slug (string), [key: string] (unknown) |
CollectionListOptions |
interface | limit? (number), offset? (number), sort? (string), filter? (Record<string, unknown>) |
CollectionListResult |
interface | entries (CollectionEntry[]), total (number) |
ScaffoldHookContext |
interface | targetDir (string), projectName (string), siteTitle (string), themeId (string), packageJson (Record<string, unknown>), dependencyMode ('workspace' |
ScaffoldHook |
interface | type (ScaffoldHookType), name (string), handler (HookHandler), priority? (number), critical? (boolean) |
HookHandler |
type | (context: ScaffoldHookContext) (Promise |
ScaffoldHookType |
type | values ('preScaffold' |
Import paths (all equivalent):
CollectionProvider—@stackwright/types·@stackwright/collectionsScaffoldHookContext,ScaffoldHook,HookHandler,ScaffoldHookType—@stackwright/types·@stackwright/hooks-registry·@stackwright/scaffold-core
Stackwright has first-class dark mode and cookie-based preference persistence:
darkColorsfield in theme YAML — same shape ascolors, used when dark mode is active.ThemeProvidermanagescolorMode('light'|'dark'|'system'), exposessetColorMode()via context. Components readtheme.colorsand get the resolved palette automatically — zero changes needed in content components.ColorModeScript(from@stackwright/themes) — a blocking<script>placed in<head>that reads thesw-color-modecookie before React hydrates, preventing flash-of-wrong-theme.StackwrightDocument(from@stackwright/nextjs) — a drop-in_document.tsxthat includesColorModeScriptautomatically.- Cookie utilities (
@stackwright/core):getCookie,setCookie,removeCookie— SSR-safe, zero dependencies. - Consent utilities (
@stackwright/core):getConsentState,setConsentState,hasConsent— IAB TCF categories (necessary,functional,analytics,marketing).
- Service Boundaries: No obvious service boundaries — all code resides within the project's monorepo.
- Data Flows: Data primarily flows from YAML configuration files → prebuild pipeline → JSON → React components via the core framework.
- External Dependencies
- Lucide React: Icon library (replaced MUI icons). Static imports registered via
registerDefaultIcons(). - Radix UI: Headless primitives powering
@stackwright/ui-shadcn(Tabs, Accordion). - Tailwind CSS: Used exclusively by
@stackwright/ui-shadcn. Core components use inlinestyle={{}}props — no Tailwind in@stackwright/core. - js-yaml: YAML parsing throughout the framework.
- Zod (v4): Runtime schema validation, JSON schema generation, and MCP tool introspection.
- Lucide React: Icon library (replaced MUI icons). Static imports registered via
- Cross-Component Communication
- Themes and color mode can be changed dynamically:
ThemeProviderexposessetThemeandsetColorModevia context. - Custom events (e.g.,
onChange) can be registered by child components to interact with parents.
- Themes and color mode can be changed dynamically:
See CONTRIBUTING.md for common issues and debugging tips.
- Framework Documentation
- React Documentation
- Next.js Documentation
- Lucide Icons
- Radix UI Primitives
- Tailwind CSS (used by
@stackwright/ui-shadcnonly)
- Development Tools
- Monorepo Management
Pages can override the site-wide sidebar defined in stackwright.yml using the navSidebar field in their content.yml.
Resolution order (highest wins):
- Page-level
navSidebarincontent.yml(explicit override) - Site-level
sidebarinstackwright.yml(default from Theme Otter) - No sidebar
Use cases:
- Dashboard pages:
navSidebar: nullto maximize content width - Documentation chapters: different sidebar with section-specific navigation
- Landing pages: inherit site sidebar from theme
YAML examples:
# Hide sidebar on this page (full-width content)
content:
navSidebar: null
content_items:
- type: main
label: "dashboard"
heading:
text: "Live Dashboard"
textSize: "h1"
# Override sidebar navigation for this page
content:
navSidebar:
navigation:
- label: "Chapter 1"
href: "/docs/chapter-1"
- label: "Chapter 2"
href: "/docs/chapter-2"
collapsed: false
content_items:
- type: text_block
label: "chapter-2-content"
textBlocks:
- text: "Chapter 2 content here..."
textSize: "body1"Otter responsibilities:
- Theme Otter sets the site-wide sidebar defaults in
stackwright.yml - Page Otter can add
navSidebaroverrides in any page'scontent.yml - If Theme Otter chose a sidebar theme, Page Otter inherits it by default (no need to repeat)
This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work- Use
bdfor ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run
bd primefor detailed command reference and session close protocol - Use
bd rememberfor persistent knowledge — do NOT use MEMORY.md files
Architecture in one line: issues live in a local Dolt DB; sync uses refs/dolt/data on your git remote; .beads/issues.jsonl is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
When ending a work session, you MUST complete ALL steps below. Work is NOT complete until git push succeeds.
MANDATORY WORKFLOW:
- File issues for remaining work - Create issues for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- PUSH TO REMOTE - This is MANDATORY:
git pull --rebase git push git status # MUST show "up to date with origin" - Clean up - Clear stashes, prune remote branches
- Verify - All changes committed AND pushed
- Hand off - Provide context for next session
CRITICAL RULES:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds