New: Build with SOLAR

<Solar turns a bad prop into a fix an agent can act on. />

solar_1200x600.png

When an AI agent writes a React component, nothing checks whether the props it generated are actually valid. React never enforced that at runtime by default, and as of React 19, that's even more true: the old PropTypes escape hatch was removed from the package entirely, and passing the wrong type is now silently ignored rather than logging so much as a warning. That's a reasonable choice for React to make. Runtime checks cost something on every render, and TypeScript already catches most of this at compile time for the humans writing components by hand.

For an agent, though, most of the time nothing happens right away. The bad value just sits there until something downstream trips over it, an onClick that turns out to be a string instead of a function, a .map() call on something that isn't an array, and by the time that throws, the error points at the crash site, not the prop that caused it. I build enough with Claude Code and Cursor to hit this constantly: an agent generates a component, mounts it with the wrong prop shape, and either nothing visibly breaks or something breaks three components downstream from the actual mistake.

So I built Solar, a runtime UI framework where a bad prop always throws the same thing, immediately, at the point of the mistake: a structured error object with a fix field the agent can read and act on directly. That's the whole idea. Everything else in the framework exists to support it.

Three ways agent-written UI code breaks differently

AI-generated code fails differently than human-written code, and the difference matters more the larger a share of your codebase an agent is writing. It's correct file by file but inconsistent across files, because nothing enforces the conventions a human would pick up without being told. It doesn't respect conventions it can't see: a developer learns "this codebase always types onClick as a function" from reading the surrounding code, but a model regenerating a file from scratch has no such context unless the framework encodes it directly. And it gets regenerated wholesale far more often than it gets incrementally maintained. Internal tools built by an agent tend to get rebuilt when requirements change, not patched line by line, so mistakes that would get caught in code review during ordinary maintenance ship straight to the browser instead.

React, Vue, and Svelte were designed on the assumption that a human writes, reads, and maintains every file. That assumption is what's breaking. The frameworks aren't wrong for what they were built for.

Every other layer of the stack already solved this

APIs have OpenAPI and JSON Schema. Databases have schemas and constraints. Typed languages have compilers. Each one gives a machine a structured description of what's valid before something breaks, and a structured error after. UI frameworks never needed that, because a human was always in the loop to read whatever surfaced, a warning, a wrong render, an eventual crash, and connect it back to the actual mistake.

Solar treats "a model will read this API" as the primary design constraint, instead of retrofitting typed props onto a framework built for human authors. It's a from-scratch runtime, not a layer bolted onto React.

The build order gives away what mattered first. ContractError, the structured error format, came before anything else, because every other piece depends on having somewhere to report through. Then the component registry, so a model can discover what's valid before it generates code instead of finding out after it already guessed wrong. Then typed slot composition, which extends the same contract model to parent-child relationships. Then the compact h() array notation, once the registry existed to make a token-efficient generation target worth having. Errors first, discovery second, composition third, efficiency last.

How it works

Every component is an explicit contract

defineComponent declares a component's props as a schema, not a convention. Each prop gets a type, whether it's required, a default, and an optional enum restricting it to specific values. Props are validated at the component boundary before render, every time, not just in a dev build.

import { createElement, defineComponent, registry } from './framework/index.js'

const Button = defineComponent({
  name: 'Button',
  props: {
    label: { type: 'string', required: true },
    onClick: { type: 'function', required: true },
    variant: { type: 'string', enum: ['primary', 'secondary'], default: 'primary' },
  },
  render({ label, onClick, variant }) {
    return createElement('button', { class: variant, onclick: onClick }, label)
  }
})

registry.register(Button)

A structured error instead of a stack trace

Pass the wrong type and Solar throws a ContractError, a plain object with component, prop, expected, received, and a fix field written as an instruction, not a description.

Button({ label: 42, onClick: () => {} })
// throws ContractError:
{
  "error": "ContractError",
  "component": "Button",
  "prop": "label",
  "expected": "string",
  "received": "number",
  "fix": "Pass a string value for \"label\""
}

An agent catches this, reads fix, and retries. No human relays the error back.

A manifest a model reads before it writes anything

Every component self-registers on import. registry.manifest() returns the full catalog as JSON, every component's name, props, types, enums, and defaults, in one call.

registry.manifest()
// →
[
  {
    "name": "Button",
    "props": {
      "label": { "type": "string", "required": true },
      "onClick": { "type": "function", "required": true },
      "variant": { "type": "string", "enum": ["primary", "secondary"], "default": "primary" }
    }
  },
]

Feed this to an agent as system context and it never has to guess an API surface it hasn't seen. It reads the manifest, generates props against that schema, and only reaches ContractError if the schema changed underneath it.

Composition gets the same treatment

A slot prop restricts which component type can fill it. Passing a plain DOM node where a Button vnode is expected throws the same way a wrong string does, at the parent boundary, by name, instead of failing deep inside a render nobody's watching.

const Card = defineComponent({
  name: 'Card',
  props: {
    title: { type: 'string', required: true },
    action: { type: 'slot', accepts: 'Button', required: true },
  },
  render({ title, action }) {
    return createElement('div', { class: 'card' },
      createElement('h3', {}, title),
      action,
    )
  }
})

// valid
Card({ title: 'Hello', action: Button({ label: 'Go', onClick: () => {} }) })

// throws: Card: prop "action": expected slot(Button), got vnode with no _source
Card({ title: 'Hello', action: createElement('button', {}, 'Go') })

A compact notation built as a generation target

h() parses a dense array format into the same vnode tree createElement produces, and it's registry-aware, so a component name in the array resolves and dispatches automatically.

import { h } from './framework/index.js'

h(['Button', { label: 'Save', onClick: handleSave, variant: 'primary' }])
// resolves to the registered Button component, prop validation still runs

The array format reads closer to markup and is cheaper to produce than nested function calls, which is a deliberate design choice: h() exists as something a model can generate directly, not just something a human finds convenient.

Three narrow effect primitives instead of one general one

State uses useState, same shape as React. But instead of a single useEffect that tries to cover data fetching, subscriptions, and lifecycle at once (and is easy to misuse with a wrong or missing dependency array), Solar splits side effects into three purpose-built hooks: useResource for async data with automatic cancellation on key change, useSubscription for event listeners that attach and detach when their source, event, or handler changes, and onMount/onUnmount for lifecycle callbacks that run exactly once.

const UserCard = defineComponent({
  name: 'UserCard',
  props: { userId: { type: 'number', required: true } },
  render({ userId }) {
    const [width, setWidth] = useState(window.innerWidth)

    const { data, loading, error } = useResource({
      key: userId,
      fetch: async (signal) => {
        const res = await fetch(`/api/user/${userId}`, { signal })
        return res.json()
      },
    })

    useSubscription({ source: window, event: 'resize', handler: () => setWidth(window.innerWidth) })

    onMount(() => analytics.track('UserCard mounted'))
    onUnmount(() => analytics.track('UserCard unmounted'))

    if (loading) return createElement('p', {}, 'Loading...')
    return createElement('p', {}, `${data.name} - viewport: ${width}px`)
  }
})

Each hook does one thing, so there's one narrower way to get it wrong. A model that misuses useResource breaks async fetching and nothing else, instead of quietly breaking a subscription cleanup it never touched, the way a wrong dependency array can in a general useEffect.

No compiler, and a rigid file layout on purpose

Solar is runtime-based. It works from a CDN <script type="module"> tag in a plain HTML file, no bundler, no build step. npm create solarbuild@latest my-app scaffolds a full project if you want one. Components live one per file, filename matches component name, default export is always the defineComponent call.

None of this is a convention a model has to infer from surrounding files. It's a rule it can follow directly: one component per file, props schema always second in the config object, event handlers always typed function, no side effects outside the declared-effect primitives.

Two ways an agent finds Solar without reading the docs first

A registry manifest only helps once an agent already knows to call it. Getting Solar in front of an agent in the first place is a separate problem, and it's the one I spent the least time on relative to how much it matters, so I built two direct paths.

The Cursor plugin loads the API straight into the editor: defineComponent, the hooks, the contract model, and the discovery pattern, so a chat session started inside a Solar project already knows the conventions before it writes a single component. Add it from Cursor Directory, no config beyond that.

solarbuild-mcp does the same job for any MCP-compatible client, Cursor, Claude, Windsurf, whatever you're running. Point it at a project's components directory:

{
  "mcpServers": {
    "solarbuild": {
      "command": "npx",
      "args": ["solarbuild-mcp", "--components", "./components"]
    }
  }
}

It exposes three tools against that project's live registry. manifest returns the full schema for every registered component. component returns one component's schema by name. validate checks a props object against a schema without mounting anything, returning the same fix-bearing errors ContractError throws at runtime. Instead of reading static docs that might be a version behind, an agent queries the live components in the project itself, wherever the codebase happens to be right now.

There's also a StackBlitz demo that runs the whole loop in the browser with nothing installed: mount a typed Button, catch a ContractError on a bad prop, read the fix field.

Where Solar fits

The strongest fit is AI-generated internal tools: a business user describes what they want, an agent generates components against an explicit contract, and nobody's reviewing every generated file line by line, so catching mistakes at the boundary matters more than it does for hand-maintained code. Past that, it fits no-code and low-code builders (a visual config drives generation on the backend, and the registry lets the platform compose components predictably without knowing their internals), vibe-coded apps built entirely through prompting, configurators and form builders generated from a data schema or API spec, and agent-driven UIs that modify themselves at runtime in response to a user request.

The thread running through all five: someone is directing what gets built, choosing the shape, the data, the constraints, while an agent writes the component code underneath it. Solar is the runtime layer that makes that agent's output trustworthy enough to ship without someone reviewing every line. It's the same shift I wrote about in End of coding, age of building, applied to one specific layer of the stack.

Solar vs React, Vue, and Svelte

React / Vue / Svelte Solar
Prop validation Optional and warning-only at best; React removed PropTypes from the package entirely in v19 Always on, enforced at the component boundary at runtime
Failure on a bad prop Usually nothing at first; a generic JS error later, disconnected from the actual mistake A structured ContractError object with expected/received/fix
Discovering valid components Source, Storybook, or hand-written docs registry.manifest(), one JSON call
Composition safety A child can be anything; a wrong one usually fails deep in render Typed slot props reject the wrong child at the parent boundary, by name
Effect model One general useEffect, easy to misuse with a wrong dependency array Three narrow primitives, one job each
File structure Convention, enforced by team norms and review Rigid by design, nothing to infer and no reviewer required to catch drift
Build step Usually a compiler or bundler None

A developer internalizes "don't pass a number where the linter won't catch it" over months of working in a codebase. An agent doesn't, unless the framework tells it in the same turn it made the mistake, in a format it can parse without a person relaying the error back.

Where it sits next to the other AI-first UI systems

Solar isn't the only project chasing this idea. Google's A2UI, open-sourced in December 2025 and already adopted by CopilotKit and Flutter's GenUI work, encodes UI as a flat adjacency list referencing a pre-approved component catalog, which prevents an agent from executing arbitrary code at the architecture level. OpenUI is closest to Solar architecturally: a token-efficient streaming language with Zod schema enforcement on props, the rough equivalent of Solar's props object, also restricted to a pre-registered component library. Storybook's MCP server takes a different angle entirely: it layers contract enforcement on top of an existing component system, with a self-healing loop where an agent generates a component, writes stories, runs interaction and accessibility tests, and fixes what fails, rather than baking contracts into the framework itself.

The academic side is converging on the same idea from a different direction. SpecifyUI extracts structured specifications from UI references so generation stays anchored to intent instead of drifting with each prompt. Generative Interfaces for Language Models, out of Stanford's SALT Lab, found generative interfaces preferred over conversational ones by up to 72% in human evaluation. BISCUIT, from Apple's ML research group, scaffolded LLM-generated code with ephemeral UIs inside Jupyter notebooks back in 2024, which is the earliest version of this argument I've found: structured intermediate representations beat free-form generation once a model is the one producing the output.

Where Solar differs from A2UI and OpenUI: its registry is informational, for discovery and validation, not a hard execution sandbox restricting the model to a pre-approved catalog. That's easier to drop into an existing codebase, at the cost of the stronger safety guarantee those two offer. It also has no dependency on an existing framework or toolchain, where OpenUI and Storybook MCP both sit on top of one. And it's small: the entire core is a handful of files, easier to read end to end than a full generative UI platform, which makes it a legible base for testing the contract-enforcement idea itself rather than a production platform you adopt wholesale. The narrower feature set (no streaming-optimized wire format, no adoption yet by frameworks like CopilotKit or AG-UI, a smaller ecosystem) is the cost of that legibility. I built it that way on purpose, and it's not resolved in Solar's favor yet.

What Solar isn't

It's not a meta-framework. No SSR, no file-based routing, no build pipeline. It doesn't replace React either. It solves a narrower problem, under a different constraint. And it works fine for a human writing components by hand, even though that's not the primary thing it's optimized for.

A few things the project hasn't settled. Whether the prop schema should stay a runtime object or move to a TypeScript interface, trading developer experience for a more reliable target for a model to generate against. How the contract model should extend across schema boundaries as composition gets deeper. And whether a dev server surfacing validation errors before runtime is worth the build-step tradeoff it would reintroduce.

Try it

npm create solarbuild@latest my-app
cd my-app
npm run dev

Or skip the install entirely and drop this into an empty HTML file:

<script type="module">
  import { defineComponent, mountComponent, registry, useState, createElement } from 'https://cdn.jsdelivr.net/npm/solarbuild/framework/index.js'

  const Counter = defineComponent({
    name: 'Counter',
    props: {},
    render() {
      const [count, setCount] = useState(0)
      return createElement('button', { onclick: () => setCount(n => n + 1) }, `Clicked ${count} times`)
    }
  })

  registry.register(Counter)
  mountComponent(Counter, {}, document.getElementById('app'))
</script>

If you're building anything where an agent writes the UI instead of a person, that's the one decision worth taking even if you never touch Solar itself: make your framework's failure mode a structured error with a fix field an agent can parse, not a generic runtime error three components away from the actual mistake.

Developer Writing Assistant

Handbook
Developer Marketing Handbook

Goals

Developer marketing builds trust first, pipeline second.
The work connects your product to how developers actually build and helps that credibility translate into adoption and revenue.

A great developer experience is the foundation. It starts with discoverability, continues through docs, and carries into the product itself. Good documentation shortens time to value and builds confidence that your product can scale with real teams. Developers trust what they can inspect, so show how the product works and let the system speak for itself.

Success isn't clicks or vanity metrics. It's measurable engagement that creates product-qualified leads, builds influence across teams, and contributes to both product-led and sales-led growth.
When developers use your product by choice and advocate for it inside their company, you've done the job right.

Strategy

Start with reality, not aspiration.

Map where your product fits in the developer workflow, then help them do that job faster or with less friction.

Lead with clarity. Explain what it is, what it does, and why it matters.

Show the system behind the product. Architecture, examples, and tradeoffs explain more than positioning ever will.
If you can do it in a clever or playful way that still feels authentic, that's bonus points.

The best developer marketing respects time, delivers value, and makes something complex feel obvious.

Journey

Awareness → Evaluation → Adoption → Advocacy.
Each stage should connect clearly to the next.

Awareness happens in places developers already spend time: GitHub, Reddit, newsletters, blogs.
Evaluation happens in your docs, demos, and sandboxes.

For most developers, the docs are the real homepage, so accuracy and structure matter more than polish.

Adoption depends on how fast they reach first success.
Advocacy is when they start teaching others what they learned from you.

Personas

Create personas based on who buys the product and who actually uses it. For example:

Buyers: CTO or Engineering Leader, Senior Engineer, Implementation Architect.
Users: Frontend, Full-stack, App Developer.
Adjacent: Ops, Product, Design.

Each persona has different pain points and goals.
CTOs and Engineering Leaders care about governance and ROI.
Senior Engineers look for performance, flexibility, and code quality.
Implementation Architects focus on how well a tool integrates and scales.
Write for what each person owns, not what you wish they cared about.

These categories are shifting. PMs and designers who build with AI tools aren't adjacent anymore. They're users. Update your personas to reflect how people actually work, not how the org chart defines them.

Messaging

Be clear first. Be clever only if it helps.
Make every message easy to scan. Lead with the point before expanding on it.
Good developer messaging is specific, practical, and rooted in how people actually build.

Clarity earns trust, but a bit of personality makes it stick.
The goal isn't to sound like marketing. It's to communicate something real that developers recognize and care about.

Build around three pillars:

  • Speed: faster builds, fewer tickets
  • Efficiency: consolidated stack, lower maintenance
  • Control: safe scale, long-term confidence

If you can back it with code, data, or proof, keep it.
If it only sounds good, cut it.

Campaigns

Treat campaigns like product launches.
Plan, ship, measure, repeat.

Each campaign should answer three questions:

  • What developer problem are we solving?
  • What proof are we showing?
  • What happens next?

Treat developer feedback like bug reports and close the loop quickly when something needs to be corrected or clarified.

Make it easy for developers to try, test, or share.
Run retros on every launch and capture what worked, what didn't, and what to change next time. Always learn from what you launch.

Content

Write with clarity and intention. Every piece should help developers build faster, learn something new, or solve a real problem.

Strong content earns attention because it's useful.
Lead with the outcome or insight, then show how to get there. Make it easy to skim from top to bottom.
Show working examples, explain tradeoffs, and include visuals or code where it helps understanding. If it doesn't teach or demonstrate something real, it doesn't belong.

Core content types

  • Blog posts: tutorials, technical breakdowns, or opinionated takes grounded in experience.
  • Guides and tutorials: step-by-step instructions that lead to a working result.
  • Integration or workflow content: explain how tools connect and where they fit in a developer's process.
  • Technical guides and code examples: deeper material for experienced readers who want implementation detail.
  • Explainer or glossary content: clear, factual definitions written to answer specific questions directly.
  • Video or live sessions: demos, interviews, or walkthroughs that show real workflows.
  • Research and surveys: reports or insights that help developers understand the state of their field.

Content strategy buckets

  1. Awareness — generate buzz and discussion. Hot takes, thought leadership, or topics that invite conversation.
  2. Acquisition — bring new developers in through problem-solving content. Tutorials, guides, and explainers that answer real questions.
  3. Enablement — help existing users succeed. Deep tutorials, documentation extensions, and practical how-to content with long-term value.
  4. Convert Paid — drive upgrades or signups. Feature-specific walkthroughs or advanced use cases that show value worth paying for.

Each piece should fit into one of these buckets and serve a clear purpose. Awareness earns attention. Acquisition builds trust. Enablement drives success. Convert Paid turns success into growth.

Clarity is the standard. Use it to earn credibility.

Community

Reddit. GitHub. Discord. Slack. YouTube and other social platforms.
Join conversations, don't start pitches.

Be helpful. Add context. Share working examples.
When your content becomes the answer people link to, you've earned credibility.

Metrics

Measure adoption and revenue, not reach.
Awareness is useful, but only if it drives activation or expansion.

Focus on signals that show impact:

  • Product or API usage
  • Time to first success
  • Product-qualified leads
  • Developer-influenced revenue
  • Retention and repeat engagement

The goal is to prove that trust earned from developers shows up later in product usage and revenue.

Developer Marketing Skill

I built a Developer Marketing Skill for Claude that helps evaluate content, strategy, and campaigns against the principles in this handbook.

Use it to stress-test messaging, review technical content, plan developer campaigns, or get feedback on positioning. It applies a "trust first, pipeline second" philosophy with an emphasis on clarity, technical credibility, and measurable engagement.

Need more resources?

Check out my curated collection of developer marketing tools, newsletters, and resources.

ESC