Getting started
gengen is a schema DSL for LLM output. Define once — get a system prompt, a parser, and a React renderer.
Installation
Install the package and its peer dependencies:
npm install @moeki0/gengen react-markdown remark-gfm \
mdast-util-from-markdown mdast-util-to-markdown mdast-util-to-string
Quick example
Here is the complete flow — define a renderer, generate a prompt, render the response.
Use g.block().schema() to describe what the LLM should produce. This returns a SchemaDefinition — a plain object with no React dependency. Keep this server-safe so you can use it both in API routes and client components.
import { g } from '@moeki0/gengen'
// card.schema.ts — server-safe, no React import needed
export const cardSchema = g.block('card')
.describe('A key insight or takeaway')
.schema({
title: g.text(),
body: g.text(),
tags: g.list(),
})
Call .component() on the schema to create a RendererDefinition. This is kept separate so the schema stays importable on the server.
import { cardSchema } from './card.schema'
// card.tsx — client component with React
export const Card = cardSchema.component(({ title, body, tags }) => (
<div className="card">
<h3>{title}</h3>
<p>{body}</p>
<ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
</div>
))
3
Generate the system prompt
Import the schema (not the renderer) in your API route and pass it to g.prompt().
import { g } from '@moeki0/gengen'
import { cardSchema } from './card.schema'
export async function POST(req: Request) {
const systemPrompt = g.prompt([cardSchema])
// → "A card block (write as: ### card heading, then content below)
// — A key insight or takeaway
// - write title as a plain text paragraph
// - write body as a plain text paragraph
// - write tags as a bullet list (- item)"
// Pass systemPrompt to your LLM of choice
}
Send this as the system message alongside your user query. The LLM will structure its response to match your schema.
Import the renderer (with component) in your client component and pass it to <Gengen>.
'use client'
import { Gengen } from '@moeki0/gengen/react'
import { Card } from './card'
export default function MyPage() {
const [response, setResponse] = useState('')
async function ask(query: string) {
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ query }),
})
setResponse(await res.text())
}
return <Gengen markdown={response} renderers={[Card]} />
}
Multiple renderers
Pass an array. gengen matches each block to the most specific schema. Unmatched blocks fall back to standard Markdown rendering.
const Summary = g.block('summary')
.describe('A short 1-paragraph summary')
.schema({ text: g.text() })
.component(({ text }) => <p className="summary">{text}</p>)
const Quote = g.block('quote')
.schema({ text: g.blockquote() })
.component(({ text }) => <blockquote>{text}</blockquote>)
const systemPrompt = g.prompt([Summary, Quote])
<Gengen markdown={response} renderers={[Summary, Quote]} />
Inline renderers
Use g.inline() to define custom markers within prose text.
import { g } from '@moeki0/gengen'
import { useInlineText } from '@moeki0/gengen/react'
const Term = g.inline('term')
.marker('[[', ']]')
.describe('a technical term worth highlighting')
.component(() => {
const text = useInlineText()
return <span className="term">{text}</span>
})
// LLM writes: "The [[photosynthesis]] process converts light into energy."
// Renders the word as a styled <span>
<Gengen markdown={response} renderers={[Term]} />
Context
Pass arbitrary data through context and read it inside components with useGengenContext().
<Gengen
markdown={response}
renderers={[Card]}
context={{ onAction: handleAction }}
/>
// Inside a renderer component:
import { useGengenContext } from '@moeki0/gengen/react'
function CardComponent({ title, body }: { title: string; body: string }) {
const { onAction } = useGengenContext()
return (
<div>
<h3>{title}</h3>
<p>{body}</p>
<button onClick={() => onAction({ type: 'expand', payload: title })}>
Expand
</button>
</div>
)
}
Next steps