Renderer Architecture
The rendering pipeline:Copy
Template → Detector → Renderer → HTML String
↓
Determines type:
- HTML function
- React component
- MDX string
- Custom...
Built-in Renderers
HTML Renderer
Handles template functions returning strings:Copy
// Detection: typeof template === 'function' && returns string
const htmlTemplate = (ctx) => `<div>${ctx.output.name}</div>`;
React Renderer
Handles React components:Copy
// Detection: typeof template === 'function' && JSX return type
import { MyWidget } from './widget';
const reactTemplate = MyWidget;
MDX Renderer
Handles MDX strings:Copy
// Detection: string with JSX-like tags or {expressions}
const mdxTemplate = `# {output.title}\n<Card>{output.body}</Card>`;
Creating a Custom Renderer
1. Define the Renderer Interface
src/renderers/my-renderer.ts
Copy
import { TemplateContext } from '@frontmcp/ui';
export interface CustomRendererOptions {
// Renderer-specific options
customOption?: boolean;
}
export interface CustomRenderer {
/**
* Check if this renderer can handle the template
*/
canHandle(template: unknown): boolean;
/**
* Render the template to HTML
*/
render<In, Out>(
template: unknown,
context: TemplateContext<In, Out>,
options?: CustomRendererOptions
): string;
}
2. Implement the Renderer
src/renderers/my-renderer.ts
Copy
import { escapeHtml } from '@frontmcp/ui';
export const myCustomRenderer: CustomRenderer = {
canHandle(template: unknown): boolean {
// Check if template matches your format
if (typeof template !== 'object' || template === null) {
return false;
}
// Example: check for specific property
return 'myCustomFormat' in template;
},
render<In, Out>(
template: unknown,
context: TemplateContext<In, Out>,
options?: CustomRendererOptions
): string {
const config = template as MyCustomTemplate;
// Build HTML from your custom format
let html = '';
for (const section of config.sections) {
html += `<div class="section">${escapeHtml(section.content)}</div>`;
}
return html;
},
};
interface MyCustomTemplate {
myCustomFormat: true;
sections: Array<{ content: string }>;
}
3. Register the Renderer
src/renderers/index.ts
Copy
import { htmlRenderer, reactRenderer, mdxRenderer } from '@frontmcp/ui';
import { myCustomRenderer } from './my-renderer';
// Order matters - first matching renderer wins
export const renderers = [
myCustomRenderer, // Check custom format first
reactRenderer,
mdxRenderer,
htmlRenderer, // HTML is the fallback
];
export function detectAndRender<In, Out>(
template: unknown,
context: TemplateContext<In, Out>
): string {
for (const renderer of renderers) {
if (renderer.canHandle(template)) {
return renderer.render(template, context);
}
}
throw new Error('No renderer found for template');
}
Example: YAML Template Renderer
A renderer for YAML-based templates:src/renderers/yaml-renderer.ts
Copy
import yaml from 'js-yaml';
import { escapeHtml, TemplateContext } from '@frontmcp/ui';
interface YamlTemplate {
title?: string;
sections: Array<{
heading?: string;
content: string;
type?: 'text' | 'list' | 'code';
}>;
}
export const yamlRenderer = {
canHandle(template: unknown): boolean {
if (typeof template !== 'string') return false;
try {
const parsed = yaml.load(template);
return typeof parsed === 'object' && parsed !== null && 'sections' in parsed;
} catch {
return false;
}
},
render<In, Out>(
template: string,
context: TemplateContext<In, Out>
): string {
// Parse YAML
const config = yaml.load(template) as YamlTemplate;
// Interpolate variables
const interpolate = (str: string): string => {
return str.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
const value = getNestedValue(context, path);
return escapeHtml(String(value ?? ''));
});
};
// Build HTML
let html = '<div class="yaml-widget">';
if (config.title) {
html += `<h1>${interpolate(config.title)}</h1>`;
}
for (const section of config.sections) {
html += '<div class="section">';
if (section.heading) {
html += `<h2>${interpolate(section.heading)}</h2>`;
}
switch (section.type) {
case 'list':
html += `<ul>${interpolate(section.content)
.split('\n')
.map(li => `<li>${li}</li>`)
.join('')}</ul>`;
break;
case 'code':
html += `<pre><code>${interpolate(section.content)}</code></pre>`;
break;
default:
html += `<p>${interpolate(section.content)}</p>`;
}
html += '</div>';
}
html += '</div>';
return html;
},
};
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((o, k) => o?.[k], obj);
}
Copy
@Tool({
name: 'my_tool',
ui: {
template: `
title: "{{output.title}}"
sections:
- heading: Overview
content: "{{output.description}}"
- heading: Items
type: list
content: |
{{output.item1}}
{{output.item2}}
{{output.item3}}
`,
},
})
Example: Markdown-Only Renderer
A simple Markdown renderer without JSX:This example uses
new Function() for expression evaluation, which is equivalent to eval(). Never use this pattern with untrusted template content as it enables arbitrary code execution. For production use, implement a safe expression parser or restrict to simple property access patterns (e.g., output.name only).src/renderers/markdown-renderer.ts
Copy
import { marked } from 'marked';
import { escapeHtml, TemplateContext } from '@frontmcp/ui';
export const markdownRenderer = {
canHandle(template: unknown): boolean {
if (typeof template !== 'string') return false;
// Pure markdown (no JSX tags)
const hasJsx = /<[A-Z][a-zA-Z]*/.test(template);
const hasMarkdown = /^#|^\*|\*\*|`/.test(template);
return !hasJsx && hasMarkdown;
},
render<In, Out>(
template: string,
context: TemplateContext<In, Out>
): string {
// Interpolate variables
// WARNING: new Function() is equivalent to eval() - only use with trusted templates
const interpolated = template.replace(
/\{([^}]+)\}/g,
(_, expr) => {
try {
const fn = new Function('ctx', `with(ctx) { return ${expr}; }`);
return escapeHtml(String(fn(context)));
} catch {
return `{${expr}}`;
}
}
);
// Convert Markdown to HTML
return marked(interpolated);
},
};
Template Type Detection
The detection order is important:Copy
// Check in order of specificity
const detectTemplateType = (template: unknown): string => {
// 1. Check custom formats first
if (isYamlTemplate(template)) return 'yaml';
if (isPugTemplate(template)) return 'pug';
// 2. Check function types
if (typeof template === 'function') {
if (isReactComponent(template)) return 'react';
return 'html-function';
}
// 3. Check string formats
if (typeof template === 'string') {
if (hasMdxSyntax(template)) return 'mdx';
if (hasMarkdown(template)) return 'markdown';
return 'html-string';
}
return 'unknown';
};
Caching Compiled Templates
For performance, cache compiled templates:Copy
const templateCache = new Map<string, CompiledTemplate>();
export function renderWithCache<In, Out>(
template: string,
context: TemplateContext<In, Out>
): string {
// Generate cache key
const key = hashTemplate(template);
// Check cache
let compiled = templateCache.get(key);
if (!compiled) {
compiled = compileTemplate(template);
templateCache.set(key, compiled);
}
// Render with context
return compiled(context);
}
Best Practices
- Fail gracefully - Return error HTML, don’t throw
- Cache compiled templates - Compilation is expensive
- Escape output - Always escape user content
- Document your format - Make it easy for others to use
- Test thoroughly - Cover edge cases
- Consider performance - Rendering happens on every request

