Create or edit Flint Static UI components (TypeScript classes extending Component<T>). Use when building reusable server-rendered UI that appears across multiple pages via {{tag}} placeholders in templates.
Create or modify reusable UI components in src/components/. Components are TypeScript classes that return HTML strings, invoked via {{tag}} placeholders in templates.
escapeHtml() for safetycontent/:::html block in contentsrc/client/*.tsCreate src/components/<name>.test.ts:
import { describe, it, expect } from 'bun:test';
import { MyComponent } from './<name>.js';
describe('MyComponent', () => {
it('should render with required props', () => {
const html = MyComponent.render({ /* props */ });
expect(html).toContain('<expected-element');
});
it('should escape user content', () => {
const html = MyComponent.render({
text: '<script>alert("xss")</script>',
});
expect(html).not.toContain('<script>');
expect(html).toContain('<script>');
});
});
Create src/components/<name>.ts:
import { Component, type ComponentProps } from './component.js';
export interface MyComponentProps extends ComponentProps {
// typed props here
}
export class MyComponent extends Component<MyComponentProps> {
render(): string {
return `<div class="...">
${this.escapeHtml(this.props.someField)}
</div>`;
}
}
Rules:
Component<T> with a typed props interfacerender() returns pure HTML string — no side effectsthis.escapeHtml() on every user-supplied stringthis.classNames() for conditional CSS classes<style> tagsAt the bottom of src/components/<name>.ts, add:
import type { TagDef } from '../templates/tag-registry.js';
export const tagDefs: TagDef[] = [
{
tag: 'my-tag',
label: 'My Tag',
icon: '🔧',
description: 'One-line description of this component.',
resolve: (ctx) => {
const data = ctx.frontmatter['MyData'] as MyProps | undefined;
if (!data) return '';
return MyComponent.render(data);
},
},
];
tagDefs exports by scanning src/components/ — do NOT edit tag-engine.ts'' when frontmatter data is missing so {{#if my-tag}} suppresses the blocklabel, icon, description power the manager's component browserIf the component needs data not in TemplateContext (src/templates/template-registry.ts), add the field and populate it in src/core/builder.ts. Most components should read from ctx.frontmatter directly — only add TemplateContext fields for site-wide data (navigation, siteLabels).
The content file should provide structured YAML matching the component's props:
---
MyData:
fieldA: value
fieldB: value
---
The tagDefs resolve function reads this from ctx.frontmatter and passes it to the component. Content authors control the data; the component controls the presentation.
Add {{my-tag}} to themes/default/templates/*.html (or the active theme's template). Guard optional tags:
{{#if my-tag}}{{my-tag}}{{/if}}
bun run test:run
bun run build
src/components/<name>.test.ts with XSS escape testComponent<T> with typed props interfacerender() is pure — no side effectsthis.escapeHtml()tagDefs exported from component file; registry auto-discovers ittagDefs.resolve — frontmatter drives dataTemplateContext extended only if site-wide data needed{{#if}} guard if optionalbun run test:run passesbun run build succeedsreferences/base-class.md — Component<T> API and inherited methodsreferences/tag-registration.md — How to register tags in the enginereferences/built-in-components.md — All existing components for reference