Integrations
Patterns for embedding Gridgram in a host system. None of these are separate packages — they're conventions that compose the primitive API into whatever shape your host needs.
Import boundary reminder: the pure pipeline (
parseGg,resolveDiagramIcons,renderDiagram, …) comes from'gridgram'and runs anywhere. Filesystem / HTTP helpers (buildIconContext,loadProjectConfig) come from'gridgram/node'and only work in a Node runtime.
HTTP endpoint
A minimal service that accepts .gg source and returns SVG + diagnostics:
import { serve } from 'bun'
import { parseGg, resolveDiagramIcons, renderDiagram } from 'gridgram'
import { buildIconContext } from 'gridgram/node'
serve({
port: 3000,
async fetch(req) {
if (req.method !== 'POST') return new Response('POST /render', { status: 405 })
const source = await req.text()
const { def: rawDef, errors, icons } = parseGg(source)
if (errors.length > 0) {
return Response.json({ errors }, { status: 400 })
}
const ctx = await buildIconContext({
jsonIconsMap: icons,
def: rawDef,
docDir: process.cwd(),
})
const { def, diagnostics: iconDiags } = resolveDiagramIcons(rawDef, ctx)
const { svg, diagnostics: layoutDiags } = renderDiagram(def)
return Response.json({
svg,
diagnostics: [...iconDiags, ...layoutDiags],
})
},
})Notes:
- No per-request project config — deploy-wide settings go in a
gridgram.config.tsloaded once at startup and prepended viaresolveSettings(see Configuration). - Node.js works the same; swap
Bun.serveforhttp.createServeror Fastify / Hono.
MCP tool
A Model Context Protocol server exposing Gridgram as an agent tool. Skeleton:
import { Server } from '@modelcontextprotocol/sdk/server'
import { parseGg, resolveDiagramIcons, renderDiagram } from 'gridgram'
import { buildIconContext } from 'gridgram/node'
const server = new Server({ name: 'gridgram', version: '0.1.0' }, {
capabilities: { tools: {} },
})
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name !== 'render_diagram') throw new Error('unknown tool')
const source = req.params.arguments?.source as string
const { def: rawDef, errors, icons } = parseGg(source)
if (errors.length) {
return { content: [{ type: 'text', text: JSON.stringify({ errors }) }] }
}
const ctx = await buildIconContext({
jsonIconsMap: icons, def: rawDef, docDir: process.cwd(),
})
const { def, diagnostics: iconDiags } = resolveDiagramIcons(rawDef, ctx)
const { svg, diagnostics: layoutDiags } = renderDiagram(def)
const diagnostics = [...iconDiags, ...layoutDiags]
return {
content: [
{ type: 'text', text: JSON.stringify({ diagnostics }, null, 2) },
{ type: 'image', data: Buffer.from(svg).toString('base64'), mimeType: 'image/svg+xml' },
],
}
})The critical bit for agent usability: return the diagnostics array in the response so the LLM sees why its diagram didn't render cleanly, not just the red-tinted SVG. See Diagnostics for an example agent loop.
Headless build step
In a static-site generator or CI job, process every .gg file in a directory:
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { parseGg, resolveDiagramIcons, renderDiagram } from 'gridgram'
import { buildIconContext } from 'gridgram/node'
async function buildAll(dir: string): Promise<void> {
for (const entry of await readdir(dir, { recursive: true })) {
if (!entry.endsWith('.gg')) continue
const path = join(dir, entry)
const source = await readFile(path, 'utf-8')
const { def: rawDef, errors, icons } = parseGg(source)
if (errors.length) throw new Error(`${path}: ${errors[0].message}`)
const ctx = await buildIconContext({
jsonIconsMap: icons, def: rawDef, docDir: dirname(path),
})
const { def } = resolveDiagramIcons(rawDef, ctx)
const { svg, diagnostics } = renderDiagram(def)
if (diagnostics.length > 0) {
console.warn(`${path}:`, diagnostics.map((d) => d.message).join('\n '))
}
await writeFile(path.replace(/\.gg$/, '.svg'),
`<?xml version="1.0" encoding="UTF-8"?>\n${svg}`)
}
}The docs site in this repository uses exactly this pattern — see scripts/build-docs-examples.ts for the full version (adds PNG rasterization via sharp and a .gg / .ts parity check).
PNG rasterization
Gridgram doesn't ship sharp as a dependency — the TS API renders SVG only, and sharp is a heavy native module that belongs to the host. Install it when you need PNG:
npm install sharpimport sharp from 'sharp'
import { renderDiagram, computeRenderDimensions } from 'gridgram'
const { svg } = renderDiagram(def, { renderWidth: 2048 })
const { width, height } = computeRenderDimensions(def, { renderWidth: 2048 })
await sharp(Buffer.from(svg))
.resize(width, height)
.png()
.toFile('out.png')computeRenderDimensions and renderDiagram take the same DiagramOptions, so the aspect ratio lines up.
Alternative — the gg CLI bundles sharp (lazy-loaded into ~/.cache/gridgram/ on first PNG render). If your pipeline already has access to the binary, shelling out to gg -o out.png is often simpler than wiring sharp into your app.
Preact / browser embedding
If your host is a Preact application (or any ES-module browser app bundled by Vite / Rspack / esbuild), use the <Diagram> component:
import { Diagram } from 'gridgram'
import type { DiagramDef } from 'gridgram'
export function DiagramView({ def }: { def: DiagramDef }) {
return <Diagram def={def} renderWidth={1024} />
}Or drop to the raw VNode tree with buildDiagramTree if you want to wrap it yourself. Only the pure entry point ('gridgram') is used — safe to ship to browsers.
parseGg and resolveDiagramIcons are pure too, so a browser-side live editor only needs those plus the rendering functions. See the docs/.vitepress/theme/Editor.vue source for a complete example.
Workers / worker threads
renderDiagram is pure and thread-safe — no module-level mutable state, no globals. Each worker can import gridgram and call it independently. The Tabler icon JSON is read-only and shared safely.
Caching
For a memoized render layer:
const cache = new Map<string, string>()
function renderCached(def: DiagramDef): string {
const key = JSON.stringify(def) // good enough for equality
const cached = cache.get(key)
if (cached !== undefined) return cached
const { svg } = renderDiagram(def)
cache.set(key, svg)
return svg
}Because renderDiagram is deterministic, a cache keyed on the full input def is sound. If you've resolved icons upstream, the def's src fields are inline SVG strings — the cache key will be long but stable.
See also
- CLI — built-in one-shot rendering.
- Configuration — composing project / deploy-wide defaults.
- Diagnostics — the feedback format every integration surfaces.