Sandbox
@incubrain/foundry

Nuxt 4 layer for agent-ready marketing sites

Foundry gives you a Nuxt 4 starting point for a marketing site that records visits, intent, and errors as one signal stream. It also adds agent-friendly surfaces like MCP tools, `llms.txt`, raw markdown redirects, SEO, and Open Graph support. You extend the layer in your app, add content, and connect an external consumer to the export endpoint.

79 stars8 forksTypeScriptUpdated 12d ago
Who it's for

Builders who want a Nuxt site that captures intent and works well with Claude Code or other agents.

What it delivers

You can launch a content site that gathers useful signals and stays easy for agents to read and work with.

What it does

Signal export stream

Buffers visits, form captures, and errors in one server-side ring buffer and exposes them through `/api/_signals/export` with a cursor.

Intent capture components

Includes convert components such as `ConvertForm`, `ConvertExternal`, `ConvertInternal`, and `ConvertPricing` to turn site actions into signals.

Agent-ready content surfaces

Ships `llms.txt`, raw markdown redirects, MCP tools, and SEO/OG support so agents and search tools can inspect the site more easily.

Nuxt layer structure

Provides a reusable `layer/` package, app layouts, content schemas, and server modules that you extend in your own Nuxt app.

Reference app and test harness

Contains `examples/foundry/` as a copyable app and `playground/` for unit, Nuxt, and end-to-end tests.

How to get it

  1. 1Run
    npm install @incubrain/foundry
  2. 2Run
    # .env — this is the entire required surface
    NUXT_SIGNAL_EXPORT_TOKEN=<random-secret>   # bearer token for the export endpoint
    NUXT_PUBLIC_SITE_ID=my-site                # optional — defaults to the request host

README

Foundry

A Nuxt 4 layer for standing up a marketing site that streams the full signal of whether it is working.

Foundry is the wireframe. You write content, deploy, and wire nothing — the site captures what visitors do (visits, and intent through the convert components) and what the site itself reports (errors, warnings), buffers it server-side in one envelope, and hands it to an external consumer over an authenticated cursor endpoint. We pull it into Polaris; you can point a Grafana-class puller at the same endpoint. Foundry ends at the stream — no charts, no dashboards, no anomaly detection inside the site.

The second pillar is agent-readiness. Agentic traffic is expected to dwarf human traffic, so Foundry sites ship MCP tools, llms.txt, raw-markdown redirects, and a strong SEO/OG stack — and every signal row is classified human | agent | bot server-side, because that split is itself signal.

Stack: Nuxt 4 · Nuxt Content · Nuxt UI · Tailwind v4 · TypeScript

Quick start

npm install @incubrain/foundry
// nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@incubrain/foundry'],
})
# .env — this is the entire required surface
NUXT_SIGNAL_EXPORT_TOKEN=<random-secret>   # bearer token for the export endpoint
NUXT_PUBLIC_SITE_ID=my-site                # optional — defaults to the request host

Write content under content/, deploy, point your consumer at /api/_signals/export. See examples/foundry for a complete app.

Signal architecture

Everything the site captures — analytics events and error/warning logs — becomes one SignalRow in a capped server-side ring buffer. Nothing is pushed anywhere. An external consumer pulls with a cursor.

client events  → signal provider ─┐
client errors  → errors.client ───┼→ POST /api/_signals/ingest ─┐
                                  │                             ├→ ring buffer (unstorage)
form captures  → /api/v1/webhook ─┘                             │        ↓
server errors  → signal-errors plugin ──────────────────────────┘   GET /api/_signals/export
                                                                    ?since=<seq>&limit=<n≤2000>
                                                                    → { rows, cursor, site }

The envelope — one shape for both kinds:

SignalRow {
  id, seq, ts, site,
  kind: 'event' | 'log',
  name,
  severity?,   // logs
  visitor?,    // { class: 'human' | 'agent' | 'bot', ... } — stamped server-side
  page?, referrer?, utm?,
  review?,     // ?polaris_review=<token> — this page load only, never persisted
  data?
}
  • Client events go through useEvents() → the signal provider → a debounced batch queue, flushed with sendBeacon on pagehide.
  • Identity events are always on for every visitor: ui.click ({ target, label?, section? }), ui.section ({ section, visible }), and ui.page ({ from? }). They are content-free by construction — authored element identity and labels only, never pixel coordinates, input values, or keystrokes. Sections are named by data-section or a plain <section id>; SectionWrapper stamps it for you. data-signal-ignore excludes a subtree.
  • Client errors (window.onerror, unhandledrejection, Vue errorHandler) land in the same stream as kind: 'log' rows. So do Nitro request errors, server-side.
  • Form captures POST to /api/v1/webhook (historical route name — it is the form-capture endpoint, not a webhook sender), pass honeypot/timing and zod validation, and land as a form_submitted row. The server is the durable capture point.
  • Visitor class is derived from the request User-Agent on every ingest path. A client-supplied value is always overwritten, never trusted.
  • Cursor: seq is monotonic; consumers send back the last cursor they saw.

Pulling the stream:

curl -H "Authorization: Bearer $NUXT_SIGNAL_EXPORT_TOKEN" \
  "https://your-site.com/api/_signals/export?since=0&limit=500"
# → { rows: [...], cursor: 512, site: "my-site" }

The endpoint fails closed: with NUXT_SIGNAL_EXPORT_TOKEN unset it returns 503, never open data. Rows live in useStorage('signals') — memory by default, 100 000 rows, oldest evicted. Mount nitro.storage.signals to an fs/KV driver if they must survive a restart.

Agent-readiness

SurfaceWhat ships
/api/_healthUnauthenticated liveness+identity check for external monitors (Polaris): { ok, service: 'foundry', version, siteId, timestamp }, Cache-Control: no-store. Pure computation — no storage or content access.
MCP toolslist-pages, get-page, what-changed — auto-registered from server/mcp/tools/, served by @nuxtjs/mcp-toolkit. Visit /_mcp/tools in dev to verify.
llms.txt/llms.txt + /llms-full.txt ship by default via nuxt-llms (a real layer dependency, zero config); sections auto-populate from your content collections. Override with an llms: config in your app — see examples/foundry/nuxt.config.ts.
Raw markdownThe markdown-rewrite module writes Vercel edge redirects so Accept: text/markdown or a curl/* UA on a page URL serves /raw/<path>.md, and / serves llms.txt. No-op off Vercel and in dev.
SEO / OG@nuxtjs/seo (sitemap, robots, schema.org, link checker, canonical redirects) plus a Satori OG image component for landing pages.
RSSConfig-driven feeds from any content collection — rss: { feeds: {} } in your app config, served at /rss/{key}.

Content and components

Content is YAML and markdown, edited by whoever owns the site — not code. The layer exports zod schemas you compose in your own content.config.ts:

import { basePageSchema, baseFaqSchema, baseConfigSchema, baseNavigationSchema }
  from '@incubrain/foundry/schemas'
content/
├── pages/          Markdown pages (basePageSchema)
├── faq/            FAQ entries (baseFaqSchema)
└── config/
    ├── site.yml    Site config (baseConfigSchema)
    └── navigation.yml

baseTeamSchema and bannerSchema are exported too.

Sections are deliberately minimal. The layer ships SectionWrapper — an accessible UPageSection wrapper that emits a section_view_<id> event on intersection and sets data-testid. Concrete sections (Hero, Offer, …) live in your app, not the layer: an opinionated design system is out of scope on principle.

Convert components are the intent-capture surface — the place where a visit becomes a signal worth acting on:

ComponentCaptures
ConvertFormEmail / lead capture → /api/v1/webhook
ConvertExternalOutbound clicks (Stripe, LemonSqueezy, Cal.com)
ConvertInternalInternal CTA clicks
ConvertPricingPricing interaction
ConvertSocialSocial profile clicks
ConvertSocialShareShare actions
ConvertRssFeed subscriptions (rss module)

Three layouts ship: default, article, landing — selected per route with an appLayout route rule.

Configuration reference

Environment

VariablePurpose
NUXT_SIGNAL_EXPORT_TOKENBearer token for GET /api/_signals/export. Unset → 503.
NUXT_PUBLIC_SITE_IDSite identifier stamped on every row. Defaults to the request host.
NUXT_PUBLIC_SITE_URLCanonical site URL, consumed by @nuxtjs/seo.

That is the signal surface in full. A third signal env var means a feature that does not capture signal.

Module options (nuxt.config.ts)

events: {
  signals: {
    enabled: true,        // signal capture on/off
    capacity: 100_000,    // ring buffer size
    captureErrors: true,  // client errors → the stream
  },
  debug: true,            // echo events to the console in dev
}

Repo map

layer/              The Nuxt layer — published as @incubrain/foundry
  app/              Components, composables, layouts, pages
  modules/          events (signal capture), rss, markdown-rewrite, config, css
  server/           MCP tools, content API, caching
examples/foundry/   Reference app — the one to copy
playground/         Test harness (unit + nuxt + e2e specs)
deploy/             vercel.website.json (Dockerfile in examples/foundry)

Development

pnpm dev             # layer dev server
pnpm dev:foundry     # example app
pnpm test            # vitest (specs live in playground/test)
pnpm verify          # lint + typecheck
pnpm build           # build the layer

Releasing is documented in RELEASING.md.

Not included

Validation is not product. These are out, and staying out:

  • Email sequences — capture intent here, deliver with ConvertKit/Mailchimp
  • Authentication — an anonymous ID is enough to capture signal
  • Payment processing — external links prove payment intent
  • Dashboards and charts — the consumer owns interpretation; Foundry ends at the stream
  • Analytics vendors — no Umami/GA integration; pull the buffer instead
  • Outbound webhook notifiers — no per-platform Slack/Discord/Telegram formatters; a second destination means a second source of truth
  • Drain-adapter observability pipelines — errors ride the signal stream, not a parallel log pipe
  • An opinionated design system — AI makes design cheap; hard-coding taste into a wireframe is a liability

Links

License

MIT

Files in the repo

Repository payload30 top-level entries
  • .beads
  • .claude
  • .github
  • .verdaccio
  • .vscode
  • deploy
  • docs
  • examples
  • layer
  • playground
  • scripts
  • .env.example
  • .gitattributes
  • .gitignore
  • .npmrc
  • .nuxtrc
  • .prettierrc
  • .release-it.json
  • CHANGELOG.md
  • CLAUDE.md
  • CONTEXT.md
  • eslint.config.mjs
  • GLOSSARY.md
  • package.json
  • pnpm-lock.yaml
  • pnpm-workspace.yaml
  • README.md
  • RELEASING.md
  • VISION.md
  • vitest.config.ts

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More templates

CopilotKit/
OpenBot

Open-source AI coworkers that each get a computer of their own: a browser, files and tools, with every action decided before it happens and recorded after. Bring any AG-UI agent.

4.6k
Donchitos/
Claude-Code-Game-Studios

Turn Claude Code into a full game dev studio — 49 AI agents, 72 workflow skills, and a complete coordination system mirroring real studio hierarchy.

25k

A self-organizing Obsidian vault that gives AI coding agents persistent memory. Claude Code, Codex CLI, Gemini CLI.

4.6k
idavidov13/
agentic-playwright

Production-grade Playwright + TypeScript Scaffold for Agentic Testing. Harness for all major AI coding agents baked in.

163
gavishap/
omnia-vault

Omnia Vault - the all-in-one project brain: an Obsidian LLM wiki, Graphify code graphs, a living plan that triages new videos against itself, and a Claude Code ⇄ Codex relay. Everything your project knows, in one clonable vault.

60

🎬 Tạo video "so sánh kiến thức" ngắn tự động — HyperFrames + AI voice, 1 template nhiều chủ đề.

169