Template System Documentation
SonicJS features a modern, server-side rendering template system built with TypeScript, HTMX, Alpine.js, and TailwindCSS.
Overview
Key Features
- Server-Side Rendering (SSR) - Fast initial page loads with full HTML from the server
- Type-Safe Templates - Full TypeScript support for template data interfaces
- HTMX-Driven Interactions - Dynamic updates without complex JavaScript
- Alpine.js for Client-Side Logic - Lightweight JavaScript framework for interactivity
- TailwindCSS Design System - Utility-first CSS with dark mode support
- Component-Based Architecture - Reusable, composable template components
Technology Stack
TypeScript
Type-safe template functions
HTMX 2.0
HTML-driven interactions
Alpine.js 3.x
Reactive client-side logic
TailwindCSS
Utility-first styling
Hono.js
Server framework integration
Template Renderer
Handlebars-like engine
Template Architecture
Directory Structure
src/templates/
├── layouts/ # Page layouts
│ ├── admin-layout-v2.template.ts # Main admin layout
│ ├── admin-layout-catalyst.template.ts # Catalyst-based admin UI
│ └── docs-layout.template.ts # Documentation layout
├── pages/ # Full page templates
│ ├── admin-dashboard.template.ts # Dashboard with stats & charts
│ ├── admin-content-list.template.ts # Content listing with filters
│ ├── admin-content-edit.template.ts # Content editor
│ ├── admin-media-library.template.ts # Media management
│ ├── admin-users-list.template.ts # User management
│ └── auth-login.template.ts # Login page
├── components/ # Reusable components
│ ├── form.template.ts # Form builder
│ ├── table.template.ts # Data tables with sorting
│ ├── media-grid.template.ts # Media file grid/list
│ ├── alert.template.ts # Alert notifications
│ ├── pagination.template.ts # Pagination controls
│ ├── filter-bar.template.ts # Filter controls
│ └── logo.template.ts # Logo component
└── utils/
└── template-renderer.ts # Template rendering utility
Template Function Pattern
All templates follow this TypeScript pattern:
Template Pattern
// 1. Define the data interface
export interface MyTemplateData {
title: string
items: Array<{ id: string; name: string }>
optional?: string
}
// 2. Create the render function
export function renderMyTemplate(data: MyTemplateData): string {
return `
<div class="my-template">
<h1>${data.title}</h1>
${data.items.map(item => `
<div>${item.name}</div>
`).join('')}
</div>
`
}
Template Structure
Three-Tier Template Hierarchy
Layout (admin-layout-v2.template.ts)
└── Page (admin-dashboard.template.ts)
└── Components (table.template.ts, alert.template.ts, etc.)
1. Layouts - Define the overall page structure (header, sidebar, footer)
2. Pages - Compose components into full pages
3. Components - Reusable UI elements
Template Rendering Flow
The template renderer provides Handlebars-like functionality:
Template Renderer
import { renderTemplate, TemplateRenderer } from '../utils/template-renderer'
// Simple variable interpolation
const html = renderTemplate('Hello {{name}}!', { name: 'World' })
// Output: "Hello World!"
// Nested properties
const html = renderTemplate('{{user.name}}', { user: { name: 'John' } })
// Conditionals
const html = renderTemplate(`
{{#if isActive}}
<div>Active</div>
{{/if}}
`, { isActive: true })
// Loops
const html = renderTemplate(`
{{#each items}}
<li>{{name}}</li>
{{/each}}
`, { items: [{ name: 'Item 1' }, { name: 'Item 2' }] })
// Helper functions
const html = renderTemplate('{{titleCase field}}', { field: 'hello_world' })
// Output: "Hello World"
Renderer Features
- Variable interpolation:
{{variable}} - Nested properties:
{{user.name}} - Raw HTML:
{{{rawHtml}}} - Conditionals:
{{#if condition}}...{{/if}} - Loops:
{{#each array}}...{{/each}} - Special loop variables:
{{@index}},{{@first}},{{@last}} - Helper functions:
{{titleCase field}}
Layout System
Admin Layout v2 (with Catalyst)
The main admin layout delegates to Catalyst UI:
Admin Layout Interface
export interface AdminLayoutData {
title: string
pageTitle?: string
currentPath?: string
user?: {
name: string
email: string
role: string
}
content: string | HtmlEscapedString
scripts?: string[]
styles?: string[]
version?: string
dynamicMenuItems?: Array<{
label: string
path: string
icon: string
}>
}
export function renderAdminLayout(data: AdminLayoutData): string {
const { renderAdminLayoutCatalyst } = require('./admin-layout-catalyst.template')
return renderAdminLayoutCatalyst(data)
}
Layout Features
1. Responsive Sidebar Navigation
- Dashboard, Content, Collections, Media, Users, Plugins, Cache, Design, Logs, Settings
- Dynamic menu items from plugins
- Active state highlighting
2. Top Bar
- Logo with customizable size/variant
- Notifications button
- Background theme customizer
- User dropdown menu
3. Background Customization
- Multiple gradient themes (Deep Space, Cosmic Blue, Matrix Green, Cyber Pink)
- Custom PNG backgrounds (Blue Waves, Stars, Crescent, 3D Waves)
- Darkness adjustment slider
- Persisted to localStorage
4. Dark Mode Support
- Class-based dark mode (
dark:prefix) - System preference detection
- Toggle functionality with localStorage persistence
Component Library
1. Form Component
Form Builder
import { renderForm, FormField, FormData } from '../components/form.template'
const formData: FormData = {
id: 'my-form',
hxPost: '/api/submit',
hxTarget: '#form-response',
title: 'User Registration',
description: 'Please fill in your details',
fields: [
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
placeholder: 'you@example.com'
},
{
name: 'bio',
label: 'Biography',
type: 'textarea',
rows: 4,
helpText: 'Tell us about yourself'
},
{
name: 'country',
label: 'Country',
type: 'select',
options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' }
]
},
{
name: 'newsletter',
label: 'Subscribe to newsletter',
type: 'checkbox',
value: true
}
],
submitButtons: [
{ label: 'Save', type: 'submit', className: 'btn-primary' },
{ label: 'Cancel', type: 'button', className: 'btn-secondary', onclick: 'history.back()' }
]
}
const html = renderForm(formData)
Supported Field Types: text, email, number, date, textarea, rich_text (with EasyMDE), select, multi_select, checkbox, file
2. Table Component
Data Table
import { renderTable, TableColumn, TableData } from '../components/table.template'
const columns: TableColumn[] = [
{
key: 'name',
label: 'Name',
sortable: true,
sortType: 'string'
},
{
key: 'email',
label: 'Email',
sortable: true
},
{
key: 'created_at',
label: 'Created',
sortable: true,
sortType: 'date',
render: (value) => new Date(value).toLocaleDateString()
},
{
key: 'actions',
label: 'Actions',
render: (value, row) => `
<button hx-get="/users/${row.id}/edit" class="btn-sm btn-primary">Edit</button>
<button hx-delete="/users/${row.id}" class="btn-sm btn-danger">Delete</button>
`
}
]
const tableData: TableData = {
columns,
rows: users,
selectable: true,
rowClickable: true,
rowClickUrl: (row) => `/users/${row.id}`,
emptyMessage: 'No users found'
}
const html = renderTable(tableData)
Table Features: Column sorting (string, number, date, boolean), row selection with "select all", clickable rows, empty state, custom cell rendering
3. Alert Component
Alert Notifications
import { renderAlert } from '../components/alert.template'
// Success alert
const success = renderAlert({
type: 'success',
title: 'Success!',
message: 'Your changes have been saved.',
dismissible: true
})
// Error alert
const error = renderAlert({
type: 'error',
message: 'An error occurred. Please try again.'
})
// Warning and info
const warning = renderAlert({ type: 'warning', message: 'This action cannot be undone.' })
const info = renderAlert({ type: 'info', message: 'You have 3 pending notifications.' })
Alert Types: success, error, warning, info
4. Pagination Component
Pagination
import { renderPagination } from '../components/pagination.template'
const pagination = renderPagination({
currentPage: 2,
totalPages: 10,
totalItems: 195,
itemsPerPage: 20,
startItem: 21,
endItem: 40,
baseUrl: '/admin/content',
queryParams: { status: 'published', model: 'blog' },
showPageNumbers: true,
maxPageNumbers: 5,
showPageSizeSelector: true,
pageSizeOptions: [10, 20, 50, 100]
})
Pagination Features: Page number navigation, previous/next buttons, page size selector, query parameter preservation, mobile-responsive
HTMX Integration
HTMX enables dynamic, server-driven interactions without writing JavaScript.
Core HTMX Patterns
Auto-Refresh with Polling
<!-- Refresh stats every 30 seconds -->
<div
id="stats-container"
hx-get="/admin/api/stats"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div>Loading...</div>
</div>
Form Submission
<form
hx-post="/admin/content/create"
hx-target="#form-response"
hx-swap="innerHTML"
hx-indicator="#loading-spinner"
>
<input type="text" name="title" required />
<button type="submit">
Submit
<div id="loading-spinner" class="htmx-indicator">Saving...</div>
</button>
</form>
<div id="form-response"></div>
Delete with Confirmation
<button
hx-delete="/admin/content/${id}"
hx-confirm="Are you sure you want to delete this?"
hx-target="closest .content-item"
hx-swap="outerHTML"
>
Delete
</button>
HTMX Attributes Reference
| Attribute | Purpose | Example |
|---|---|---|
hx-get | GET request | hx-get="/api/data" |
hx-post | POST request | hx-post="/api/create" |
hx-put | PUT request | hx-put="/api/update/123" |
hx-delete | DELETE request | hx-delete="/api/delete/123" |
hx-target | Where to insert response | hx-target="#result" |
hx-swap | How to insert | innerHTML, outerHTML, beforeend |
hx-trigger | When to trigger | click, load, change, every 30s |
hx-indicator | Loading indicator | hx-indicator="#spinner" |
hx-confirm | Confirmation dialog | hx-confirm="Are you sure?" |
Alpine.js Integration
Alpine.js provides reactive client-side interactions.
Alpine.js Examples
Dropdown Menu
<div x-data="{ open: false }">
<button @click="open = !open">
Toggle Menu
</button>
<div x-show="open" @click.away="open = false">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
</div>
Tabs
<div x-data="{ activeTab: 'overview' }">
<div class="tabs">
<button @click="activeTab = 'overview'" :class="{ 'active': activeTab === 'overview' }">
Overview
</button>
<button @click="activeTab = 'details'" :class="{ 'active': activeTab === 'details' }">
Details
</button>
</div>
<div x-show="activeTab === 'overview'">
Overview content
</div>
<div x-show="activeTab === 'details'">
Details content
</div>
</div>
TailwindCSS & Design System
TailwindCSS Configuration
SonicJS uses Tailwind via CDN with custom configuration:
Tailwind Config
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#465FFF',
secondary: '#212A3E',
dark: '#1C1C24',
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
info: '#3B82F6'
},
fontFamily: {
satoshi: ['Satoshi', 'sans-serif']
}
}
}
}
Common Utility Patterns
Form Inputs
<input
type="text"
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
/>
Buttons
<!-- Primary Button -->
<button class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
Save
</button>
<!-- Secondary Button -->
<button class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
Cancel
</button>
Dark Mode Implementation
Dark Mode Toggle
Dark mode uses Tailwind's class strategy:
Dark Mode Toggle
// Dark mode toggle (in layout)
function toggleDarkMode() {
document.documentElement.classList.toggle('dark')
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'))
}
// Initialize from localStorage
if (localStorage.getItem('darkMode') === 'true' ||
(!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
Dark Mode Utilities
Every component uses dark mode variants:
Dark Mode Variants
<!-- Text colors -->
<p class="text-zinc-950 dark:text-white">Primary text</p>
<p class="text-zinc-500 dark:text-zinc-400">Secondary text</p>
<!-- Backgrounds -->
<div class="bg-white dark:bg-zinc-900">Content</div>
<div class="bg-zinc-50 dark:bg-zinc-800">Subtle background</div>
<!-- Borders -->
<div class="border border-zinc-950/10 dark:border-white/10">Bordered</div>
<!-- Rings (focus states) -->
<input class="ring-1 ring-zinc-950/10 dark:ring-white/10 focus:ring-zinc-950 dark:focus:ring-white" />
Creating Custom Templates
Step-by-Step Guide
Step 1: Define Data Interface
// src/templates/pages/my-custom-page.template.ts
export interface MyCustomPageData {
title: string
items: Array<{
id: string
name: string
description?: string
}>
user?: {
name: string
email: string
role: string
}
version?: string
}
Step 2: Create Component
// src/templates/components/my-component.template.ts
export interface MyComponentData {
title: string
items: string[]
}
export function renderMyComponent(data: MyComponentData): string {
return `
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">
${data.title}
</h3>
<ul class="space-y-2">
${data.items.map(item => `
<li class="text-sm text-zinc-500 dark:text-zinc-400">${item}</li>
`).join('')}
</ul>
</div>
`
}
Step 3: Create Page Template
import { renderAdminLayout, AdminLayoutData } from '../layouts/admin-layout-v2.template'
import { renderMyComponent } from '../components/my-component.template'
import { renderAlert } from '../components/alert.template'
export function renderMyCustomPage(data: MyCustomPageData): string {
const pageContent = `
<div>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">
${data.title}
</h1>
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
Custom page description
</p>
</div>
<!-- Success Message -->
${renderAlert({
type: 'success',
message: 'Page loaded successfully!',
dismissible: true
})}
<!-- Custom Content -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
${data.items.map(item => `
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-2">
${item.name}
</h3>
${item.description ? `
<p class="text-sm text-zinc-500 dark:text-zinc-400">
${item.description}
</p>
` : ''}
<!-- HTMX Action Button -->
<button
hx-get="/api/item/${item.id}"
hx-target="#item-details"
hx-swap="innerHTML"
class="mt-4 inline-flex items-center rounded-lg bg-zinc-950 dark:bg-white px-3 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors"
>
View Details
</button>
</div>
`).join('')}
</div>
<!-- HTMX Target for Dynamic Content -->
<div id="item-details" class="mt-6"></div>
<!-- Component Usage -->
${renderMyComponent({
title: 'My Component',
items: ['Item 1', 'Item 2', 'Item 3']
})}
</div>
`
// Wrap in layout
const layoutData: AdminLayoutData = {
title: data.title,
pageTitle: data.title,
currentPath: '/admin/my-page',
user: data.user,
version: data.version,
content: pageContent
}
return renderAdminLayout(layoutData)
}
Step 4: Create Route Handler
// src/routes/admin.routes.ts
import { renderMyCustomPage } from '../templates/pages/my-custom-page.template'
app.get('/admin/my-page', async (c) => {
const user = c.get('user')
// Fetch your data
const items = await db.query('SELECT * FROM items')
const pageData = {
title: 'My Custom Page',
items,
user,
version: '1.0.0'
}
return c.html(renderMyCustomPage(pageData))
})
Template Best Practices
- Always define TypeScript interfaces for template data
- Break down complex UIs into reusable components
- Include ARIA attributes and semantic HTML for accessibility
- Always escape user-provided content to prevent XSS
- Use HTMX for partial updates instead of full page reloads
- Provide fallback UI and error states
- Always provide loading skeletons