Overview
Clovie is a modern yet minimalist static site generator and web framework built on the brickworks/engine pattern. It emphasizes sensible defaults, modularity, and speed through a service-oriented architecture. Supports both static site generation and full Express server applications.
Service Architecture
All functionality is provided by services extending ServiceProvider
:
- File - File system operations (reading, writing, watching)
- Compile - Template compilation, asset bundling, and live reload injection
- Run - Build orchestration and command execution
- Route - Dynamic routing, SSR caching, and route handlers
- Server - HTTP/Express server management (conditionally loaded)
- Cache - Build caching and incremental builds
- Configurator - Configuration loading and validation
- LiveReload - File watching and browser refresh (development only)
- Database - Document-oriented database with WAL (server mode only)
Two Operating Modes
Static Mode (type: 'static'
): Traditional static site generation
Server Mode (type: 'server'
): Full Express applications with SSR
Core Features
- Template Engine Agnostic: Handlebars, Nunjucks, Pug, Mustache, or custom.
- Asset Processing: JavaScript bundling with esbuild, SCSS compilation, static asset copying.
- Development Server: Live reload with Express and file watching.
- Dynamic Routing: Powerful route system for both static and server-side page generation.
- Server-Side Rendering: Full Express applications with dynamic routes and API endpoints.
- Database Integration: Document-oriented database with Write-Ahead Logging for server applications.
- API Routes: Create RESTful APIs with collection-based data storage and automatic persistence.
- Port Management: Integrated utilities for managing development server ports.
Usage
New Project
Using Clovie CLI (Recommended)
# Create a new project (default template)
npx clovie create my-site
# Or with global install
clovie create my-site
# Use a specific template
clovie create my-site --template static
clovie create my-blog --template server
Installation
Option 1: Local Installation (Recommended)
npm install --save-dev clovie
# Use via npm scripts
npm run build
npm run dev
Option 2: Global Installation
npm install -g clovie
Building and Development
Available Commands
Build static files (production)
clovie build
# or
npm run build
Development server with live reload
clovie dev
# or (watch is aliased to dev)
clovie watch
# or
npm run dev
Production server (server mode only)
clovie serve
# or (server is aliased to serve)
clovie server
# or add to package.json:
"scripts": { "start": "clovie serve" }
Port management utilities
clovie kill --port 3000
clovie kill --common
clovie kill --check
# Kill specific ports
clovie kill -p 3000 3001
# Check which ports are in use
clovie kill --check --port 3000
Custom config file
clovie build --config custom.config.js
clovie dev -c custom.config.js
Configuration
Minimal Configuration (Recommended)
Clovie uses smart defaults and autoβdetection, so you can start with just:
export default {
data: {
title: 'My Site'
}
};
Autoβdetection includes:
views/
directory for HTML templatesscripts/main.js
for JavaScript entry pointstyles/main.scss
for SCSS entry pointassets/
directory for static files
Full Static Site Configuration
export default {
type: 'static', // Default - generates static HTML files
// Custom paths (optional - Clovie will auto-detect if not specified)
scripts: './src/js/app.js',
styles: './src/css/main.scss',
views: './templates',
partials: './partials',
assets: './public',
outputDir: './build',
// Development server options
port: 3000,
host: '0.0.0.0',
// Your data
data: {
title: 'My Site'
},
// Template engine (optional - defaults to Nunjucks)
// Simple string-based configuration:
renderEngine: 'nunjucks', // 'nunjucks' | 'handlebars' | 'pug' | 'eta'
// Or custom engine with render and register functions:
// renderEngine: {
// render: (template, data) => yourEngine(template, data),
// register: (name, template) => yourEngine.registerPartial(name, template)
// }
};
Server Mode Configuration
import express from 'express';
export default {
type: 'server', // Server-side application
port: 3000,
host: '0.0.0.0',
// Database configuration (auto-enabled in server mode)
dbPath: './data', // Directory for database files
walPath: './data', // Directory for WAL files
// Global data available to all routes
data: {
title: 'My App',
version: '1.0.0'
},
// Express middleware (runs before all routes)
middleware: [
express.json(),
express.urlencoded({ extended: true })
],
// Server lifecycle hooks
hooks: {
onRequest: async (ctx) => {
// Runs on every request
console.log(ctx.req.method, ctx.req.url);
},
preHandler: async (ctx) => {
// Runs before route handler
},
onError: async (ctx, error) => {
// Custom error handling
console.error(error);
}
},
// API routes (JSON endpoints)
api: [{
path: '/api/users',
method: 'GET',
handler: async (ctx, database) => {
const users = database.collection('users');
const allUsers = users.keys().map(id => ({
id,
...users.get([id])
}));
return ctx.respond.json({ users: allUsers });
}
}],
// Page routes (server-side rendered)
routes: [{
path: '/user/:id',
template: './views/profile.html',
data: async (ctx, database) => {
const users = database.collection('users');
const user = users.get([ctx.params.id]);
return { user };
}
}]
};
Advanced Features
Database (Server Mode)
Clovie automatically provides a document-oriented database with Write-Ahead Logging (WAL) in server mode. Built on @brickworks/database-ext
, it provides collections, automatic checkpointing, and crash recovery.
Database Configuration
export default {
type: 'server',
// Database configuration (automatically created if they don't exist)
dbPath: './data', // Directory for database files
walPath: './data', // Directory for WAL files
api: [{
path: '/api/users',
method: 'GET',
handler: async (ctx, database) => {
// Database is automatically provided
const users = database.collection('users');
// Get all user IDs and fetch their data
const allUsers = users.keys().map(id => users.get([id]));
return ctx.respond.json({ users: allUsers });
}
}]
};
Database Features:
- π Collection-based - Organize data into named collections
- β‘ Write-Ahead Logging - Fast writes with crash recovery
- π Automatic checkpointing - Periodic state snapshots
- πΎ JSON persistence - Human-readable file format
- π― Auto-generated IDs - Unique document identifiers
- π Query capabilities - Find documents by field values
API Routes (Server Mode)
API routes return JSON data and are perfect for building REST APIs. They're separate from template routes and don't render HTML.
API Routes Configuration
export default {
type: 'server',
api: [
{
path: '/api/status',
method: 'GET',
handler: async (ctx, database) => {
return ctx.respond.json({
status: 'ok',
timestamp: new Date().toISOString()
});
}
},
{
path: '/api/users/:id',
method: 'GET',
handler: async (ctx, database) => {
const userId = ctx.params.id;
const users = database.collection('users');
const user = users.get([userId]);
if (!user) {
return ctx.respond.json({ error: 'Not found' }, 404);
}
return ctx.respond.json({ user });
}
},
{
path: '/api/users',
method: 'POST',
handler: async (ctx, database) => {
const { name, email } = ctx.body;
const users = database.collection('users');
// add() auto-generates a unique ID
const userId = users.add({
name,
email,
created: new Date().toISOString()
});
return ctx.respond.json({
success: true,
id: userId,
user: users.get([userId])
});
}
},
{
path: '/api/users',
method: 'GET',
handler: async (ctx, database) => {
const users = database.collection('users');
// Get all user documents
const allUsers = users.keys().map(id => ({
id,
...users.get([id])
}));
return ctx.respond.json({
users: allUsers,
total: allUsers.length
});
}
}
]
};
Available Response Methods:
ctx.respond.json(data, status)
- Send JSON responsectx.respond.html(html, status)
- Send HTML responsectx.respond.text(text, status)
- Send plain text
Context Properties:
ctx.req
- Request objectctx.params
- Route parameters (e.g., :id)ctx.query
- Query string parametersctx.body
- Request body (with middleware)
Collection Operations
// Get a collection
const users = database.collection('users');
// Add with auto-generated ID
const userId = users.add({
name: 'Alice',
email: 'alice@example.com'
});
// Returns: 'doc_abc123'
// Get by ID (uses path arrays)
const user = users.get([userId]);
// Set with specific key
users.set(['alice'], {
name: 'Alice',
email: 'alice@example.com'
});
// Update a document
users.update([userId], user => ({
...user,
lastSeen: new Date().toISOString()
}));
// Check if exists
const exists = users.has([userId]);
// Get all keys
const allIds = users.keys();
// Remove a document
users.remove([userId]);
// Query by field value
const alice = users.findWhere('name', '===', 'Alice');
// Find all matching
const admins = users.findAllWhere('role', '===', 'admin');
// Nested collections
const posts = database.collection('posts');
const comments = posts.collection('comments');
comments.set([postId, 'comment1'], {
text: 'Great post!'
});
Built-in System Routes
Server mode automatically provides these system endpoints:
GET /health
- Health check endpointGET /api/info
- Server information and route count
Async Data Loading (Static Mode)
export default {
type: 'static',
// ... other config
data: async () => {
// Fetch data from API at build time
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
return {
title: 'My Blog',
posts: posts,
timestamp: new Date().toISOString()
};
}
};
Dynamic Routes (Static Mode)
export default {
type: 'static',
data: {
title: 'My Blog',
posts: [
{ id: 1, title: 'First Post', content: 'Hello World', slug: 'first-post' },
{ id: 2, title: 'Second Post', content: 'Another post', slug: 'second-post' },
{ id: 3, title: 'Third Post', content: 'Yet another', slug: 'third-post' }
]
},
routes: [{
name: 'Blog Posts',
path: '/posts/:slug',
template: 'post.html',
repeat: (state) => state.get(['posts']), // Array of items to create pages for
data: (state, post) => ({
...post,
excerpt: post.content.substring(0, 100) + '...',
date: new Date().toISOString()
})
}]
};
Static Mode Routes: Generate HTML files at build time using the repeat
function to create multiple pages from data.
Output:
posts/first-post.html
posts/second-post.html
posts/third-post.html
Dynamic Routes (Server Mode)
export default {
type: 'server',
routes: [{
name: 'User Profile',
path: '/user/:id',
template: './views/profile.html',
data: async (ctx, database) => {
// Server-side: rendered on each request
// ctx contains req, params, query
const userId = ctx.params.id;
const users = database.collection('users');
const user = users.get([userId]);
return {
user: user || { name: 'Unknown User' },
title: `Profile - ${user?.name || 'Unknown'}`
};
}
}]
};
Server Mode Routes: Rendered on each request with access to database and request context. Results are cached for performance.
Key Differences:
- No
repeat
function - routes match dynamically data
receives(ctx, database)
instead of(state, item)
- Access to request parameters, query strings, and database
- Rendered on-demand with intelligent caching
Data Transformation in Routes
export default {
// ... other config
routes: [{
name: 'Products',
path: '/products/:slug',
template: 'product.html',
repeat: (state) => state.get(['products']),
data: (state, product) => ({
...product,
price: `$${product.price.toFixed(2)}`,
slug: product.name.toLowerCase().replace(/\s+/g, '-'),
inStock: product.quantity > 0,
relatedProducts: state.get(['products']).filter(p =>
p.category === product.category && p.id !== product.id
)
})
}]
};
Route Pagination
export default {
// ... other config
routes: [{
name: 'Blog Pagination',
path: '/blog/:page?',
template: 'blog.html',
paginate: 5, // 5 posts per page
repeat: (state) => state.get(['posts']),
data: (state, posts, pageInfo) => ({
posts,
pagination: pageInfo,
title: `Blog - Page ${pageInfo.current}`
})
}]
};
Output:
blog.html
β First 5 posts (page 1)blog/2.html
β Next 5 posts (page 2)blog/3.html
β Remaining posts (page 3)
Template (post.html
):
<!DOCTYPE html>
<html>
<head>
<title>{{title}} - {{../title}}</title>
</head>
<body>
<article>
<h1>{{title}}</h1>
<p>{{excerpt}}</p>
<div>{{content}}</div>
</article>
</body>
</html>
Output:
posts/first-post.html
β First post pageposts/second-post.html
β Second post pageposts/third-post.html
β Third post page
Template Engines
Clovie supports multiple template engines out of the box. Just specify the engine name!
Built-in Template Engines
export default {
// Nunjucks (default)
renderEngine: 'nunjucks'
};
// Or use Handlebars
export default {
renderEngine: 'handlebars'
};
// Or use Pug
export default {
renderEngine: 'pug'
};
// Or use Eta
export default {
renderEngine: 'eta'
};
Supported Engines:
nunjucks
- Nunjucks (Jinja2-like) (default)handlebars
- Handlebars templatingpug
- Pug (formerly Jade)eta
- Eta (EJS-like)
Note: Nunjucks is included by default. For other engines, install the package:
npm install handlebars
or npm install pug
or npm install eta
Advanced Nunjucks Configuration
export default {
renderEngine: 'nunjucks',
// Nunjucks-specific options
nunjucksOptions: {
autoescape: true,
throwOnUndefined: false,
filters: {
// Custom filters
upper: (str) => String(str).toUpperCase(),
formatDate: (date) => new Date(date).toLocaleDateString()
},
globals: {
// Global variables available in all templates
siteTitle: 'My Site',
year: new Date().getFullYear()
}
}
};
Advanced Pug Configuration
export default {
renderEngine: 'pug',
// Pug-specific options
pugOptions: {
pretty: true,
compileDebug: false
}
};
Custom Template Engine
export default {
// Custom engine with render and register functions
renderEngine: {
render: (template, data) => {
// Your custom template rendering logic
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] || match;
});
},
register: (name, template) => {
// Your custom partial registration logic
// (optional - some engines don't support partials)
console.log(`Registering partial: ${name}`);
}
}
};
Custom Engine Requirements:
render(template, data)
- Required function to render templatesregister(name, template)
- Required function to register partials/includes
Error Handling & Best Practices
Error Handling
- Missing directories: Handles missing views, scripts, or assets folders.
- File read errors: Continues processing even if individual files fail.
- Template errors: Clear error messages for compilation failures.
- Data validation: Warns about invalid data structures.
Progress Indicators
π Starting build...
π§Ή Cleaning output directory...
π Loading data...
Loaded 2 data sources
π Processing views...
Processed 5 views
π¨ Rendering templates...
Rendered 5 templates
β‘ Bundling scripts...
Bundled 1 script files
π¨ Compiling styles...
Compiled 1 style files
π¦ Processing assets...
Processed 3 asset files
πΎ Writing files...
β
Build completed in 45ms
AutoβDiscovery
π Auto-detected views directory: views
π Auto-detected scripts entry: scripts/main.js
π Auto-detected styles entry: styles/main.scss
π Auto-detected assets directory: assets
Best Practices
- Use partial templates (files starting with
_
) for reusable components. - Validate data structures before passing to routes.
- Handle async data with proper error catching in route data functions.
- Use meaningful route paths for SEO and organization.
- Transform data in route data functions, not in templates.
- Separate static and dynamic routes for better performance.
Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
Clovie Project Structure
clovie/
βββ __tests__/ # Test files
βββ bin/ # CLI executables
β βββ cli.js # Main command line interface
β βββ kill-port.js # Port management utility
βββ config/ # Configuration files
β βββ clovie.config.js # Default configuration
βββ lib/ # Service-based architecture
β βββ createClovie.js # Engine factory function
β βββ Run.js # Build orchestration service
β βββ Cache.js # Caching service
β βββ Compile.js # Template compilation service
β βββ Configurator.js # Configuration service
β βββ File.js # File system service
β βββ LiveReload.js # Live reload service
β βββ Server/ # Server components
β β βββ Server.js # HTTP server service
β β βββ Router.js # Route handler service
β β βββ Kernel.js # Request routing kernel
β β βββ adapters/ # Server adapters
β β βββ utils/ # Server utilities
β βββ utils/ # Utility functions
β βββ clean.js # Directory cleaning
β βββ loadRenderEngine.js # Template engine loader
β βββ liveReloadScript.js # Live reload
β βββ outputPath.js # Path formatting
β βββ progress.js # Progress tracking
β βββ tasks.js # Task management
β βββ transformConfig.js # Config transformation
βββ templates/ # Project templates
β βββ default/ # Minimal starter template
β βββ static/ # Static site template
β βββ server/ # Server application template
βββ examples/ # Configuration examples
Troubleshooting
Common Issues
"Views directory does not exist"
- Ensure the
views
path in your config is correct. - Create the
views
directory if it does not exist.
"Route repeat function must return an array"
- Check that your route's repeat function returns an array.
- Ensure the data structure matches the route configuration.
"Maximum directory depth exceeded"
- Check for circular symlinks or extremely deep directory structures.
- The limit is 50 levels deep (configurable in code).
Build failures
- Check console output for specific error messages.
- Verify all referenced files exist.
- Ensure template syntax matches your compiler.
License
MIT
Available Templates:
default
- Basic static site with minimal setupstatic
- Full-featured static site with navigation and multiple pagesserver
- Full-stack server application with API routes, database, and SSR