gengen/docs

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.

1

Define the schema

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(),
  })
2

Add a React component

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.

4

Render the response

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

API reference →