Static HTML Templates
For widgets that don’t need dynamic data, use static HTML:
@Tool({
name: 'show_help',
ui: {
template: `
<div class="p-6 bg-blue-50 rounded-xl">
<h2 class="text-xl font-bold text-blue-900">Need Help?</h2>
<p class="text-blue-700 mt-2">
Contact support at [email protected]
</p>
<a href="https://docs.example.com"
class="inline-block mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg">
View Documentation
</a>
</div>
`,
},
})
Static templates are auto-detected when the template is a plain string
without JSX tags or variable interpolation.
External Template Files
Keep templates in separate files for better organization:
<div class="help-widget">
<h2>Help Center</h2>
<nav>
<a href="/docs/getting-started">Getting Started</a>
<a href="/docs/api">API Reference</a>
<a href="/docs/faq">FAQ</a>
</nav>
</div>
import { readFileSync } from 'fs';
import { join } from 'path';
const helpTemplate = readFileSync(
join(__dirname, '../widgets/help.html'),
'utf-8'
);
@Tool({
name: 'show_help',
ui: {
template: helpTemplate,
},
})
Template Wrappers
Create a custom wrapper for consistent styling:
@Tool({
name: 'my_tool',
ui: {
template: (ctx) => `<p>${ctx.output.message}</p>`,
wrapper: (content, ctx) => `
<div class="my-custom-wrapper">
<header class="wrapper-header">
<h1>${ctx.helpers.escapeHtml(ctx.output.title)}</h1>
</header>
<main class="wrapper-content">
${content}
</main>
<footer class="wrapper-footer">
Powered by FrontMCP
</footer>
</div>
`,
},
})
Handlebars Integration
Use Handlebars for logic-less templates:
import Handlebars from 'handlebars';
// Register helpers
Handlebars.registerHelper('formatDate', (date) => {
return new Date(date).toLocaleDateString();
});
Handlebars.registerHelper('currency', (amount) => {
return `$${amount.toFixed(2)}`;
});
// Create template
const source = `
<div class="order-summary">
<h2>Order #{{output.id}}</h2>
<p>Date: {{formatDate output.date}}</p>
<ul>
{{#each output.items}}
<li>{{name}} - {{currency price}}</li>
{{/each}}
</ul>
<p class="total">Total: {{currency output.total}}</p>
</div>
`;
const template = Handlebars.compile(source);
@Tool({
name: 'get_order',
ui: {
template: (ctx) => template(ctx),
},
})
EJS Templates
Use EJS for embedded JavaScript:
import ejs from 'ejs';
const template = `
<div class="user-card">
<h2><%= helpers.escapeHtml(output.name) %></h2>
<p><%= output.email %></p>
<% if (output.isPremium) { %>
<span class="badge premium">Premium</span>
<% } %>
<ul>
<% output.projects.forEach(function(project) { %>
<li><%= project.name %></li>
<% }); %>
</ul>
</div>
`;
@Tool({
name: 'get_user',
ui: {
template: (ctx) => ejs.render(template, ctx),
},
})
Pug/Jade Templates
Use Pug for clean, indentation-based templates:
import pug from 'pug';
const template = pug.compile(`
.dashboard
h1= output.title
.stats
each stat in output.stats
.stat-card
.stat-value= stat.value
.stat-label= stat.label
if output.alerts.length
.alerts
each alert in output.alerts
.alert(class=alert.type)= alert.message
`);
@Tool({
name: 'get_dashboard',
ui: {
template: (ctx) => template(ctx),
},
})
Mustache Templates
Logic-less templating with Mustache:
import Mustache from 'mustache';
const template = `
<div class="profile">
<h2>{{output.name}}</h2>
<p>{{output.bio}}</p>
{{#output.isPremium}}
<span class="badge">Premium Member</span>
{{/output.isPremium}}
<h3>Skills</h3>
<ul>
{{#output.skills}}
<li>{{.}}</li>
{{/output.skills}}
</ul>
</div>
`;
@Tool({
name: 'get_profile',
ui: {
template: (ctx) => Mustache.render(template, ctx),
},
})
Nunjucks Templates
Mozilla’s powerful templating:
import nunjucks from 'nunjucks';
// Configure environment
const env = new nunjucks.Environment();
env.addFilter('currency', (num) => `$${num.toFixed(2)}`);
env.addFilter('date', (d) => new Date(d).toLocaleDateString());
const template = `
<div class="invoice">
<h1>Invoice #{{ output.id }}</h1>
<p>Date: {{ output.date | date }}</p>
<table>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
</tr>
{% for item in output.items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.price | currency }}</td>
</tr>
{% endfor %}
</table>
<p class="total">Total: {{ output.total | currency }}</p>
</div>
`;
@Tool({
name: 'get_invoice',
ui: {
template: (ctx) => env.renderString(template, ctx),
},
})
Building a Custom Renderer
Create your own rendering system:
src/renderers/custom-renderer.ts
import { TemplateContext } from '@frontmcp/ui';
export interface CustomTemplateConfig {
layout: 'card' | 'page' | 'minimal';
sections: Array<{
type: 'header' | 'content' | 'footer';
template: string;
}>;
}
export function renderCustomTemplate<In, Out>(
config: CustomTemplateConfig,
ctx: TemplateContext<In, Out>
): string {
const layouts = {
card: (content: string) => `
<div class="card rounded-xl shadow-lg overflow-hidden">
${content}
</div>
`,
page: (content: string) => `
<div class="page max-w-4xl mx-auto py-8">
${content}
</div>
`,
minimal: (content: string) => content,
};
const sectionTemplates = {
header: (template: string) => `
<header class="bg-gray-100 px-6 py-4 border-b">
${interpolate(template, ctx)}
</header>
`,
content: (template: string) => `
<main class="px-6 py-4">
${interpolate(template, ctx)}
</main>
`,
footer: (template: string) => `
<footer class="bg-gray-50 px-6 py-4 border-t">
${interpolate(template, ctx)}
</footer>
`,
};
const sections = config.sections
.map((s) => sectionTemplates[s.type](s.template))
.join('');
return layouts[config.layout](sections);
}
function interpolate<In, Out>(template: string, ctx: TemplateContext<In, Out>): string {
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
const value = getNestedValue(ctx, path);
return ctx.helpers.escapeHtml(String(value ?? ''));
});
}
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((o, k) => o?.[k], obj);
}
Usage:
import { renderCustomTemplate } from '../renderers/custom-renderer';
@Tool({
name: 'get_user',
ui: {
template: (ctx) => renderCustomTemplate({
layout: 'card',
sections: [
{ type: 'header', template: '<h2>{{output.name}}</h2>' },
{ type: 'content', template: '<p>{{output.bio}}</p>' },
{ type: 'footer', template: '<small>ID: {{output.id}}</small>' },
],
}, ctx),
},
})
Best Practices
- Choose based on team familiarity - Use what your team knows
- Keep templates simple - Complex logic belongs in code
- Cache compiled templates - Compile once, render many times
- Always escape output - Use each engine’s escaping features
- Test thoroughly - Template errors can be hard to debug
- Document your choice - Explain why you chose a specific engine