Overview
Clovie is a modern yet minimalist static site generator and web framework built on the Jucie/engine pattern. It emphasizes sensible defaults, modularity, and speed through a service-oriented architecture. Supports both static site generation and full-featured server applications using vanilla Node.js HTTP (Express adapter available).
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 server management with adapter pattern (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 web applications with SSR using Node.js HTTP server (Express adapter available)
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 file watching and hot module replacement.
- Dynamic Routing: Powerful route system for both static and server-side page generation.
- Server-Side Rendering: Native Node.js HTTP server with dynamic routes and API endpoints.
- Middleware Support: Express middleware with auto-adapter selection for authentication, CORS, and more.
- 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 static site (default)
npx clovie create my-site
# Or with global install
clovie create my-site
# Create a server application
clovie create my-app --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.jsfor JavaScript entry pointstyles/main.scssfor 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
export default {
type: 'server', // Server-side application (uses Node.js HTTP server)
port: 3000,
host: '0.0.0.0',
// Global data available to all routes
data: {
title: 'My App',
version: '1.0.0'
},
// Express middleware (auto-selects Express adapter)
middleware: [
express.json(),
express.urlencoded({ extended: true }),
// Authentication middleware for protected routes
(req, res, next) => {
if (req.url.startsWith('/api/protected/')) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Verify token and attach user to request
req.user = verifyJWT(token);
}
next();
}
],
// 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
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
Server Adapters
Clovie uses a vanilla Node.js HTTP server by default. You can optionally use Express for advanced middleware features.
Default HTTP Server
export default {
type: 'server',
// Uses Node.js http.createServer by default
// No dependencies required - fast and lightweight
api: [{
path: '/api/hello',
method: 'GET',
handler: async (ctx, database) => {
return ctx.respond.json({ message: 'Hello!' });
}
}]
};
Using Express Adapter
import express from 'express';
import { ExpressAdapter } from 'clovie/adapters';
export default {
type: 'server',
// Use Express adapter for advanced middleware
adapter: 'express', // or new ExpressAdapter()
// Express middleware
middleware: [
express.json(),
express.urlencoded({ extended: true }),
express.static('public')
],
api: [{
path: '/api/hello',
method: 'POST',
handler: async (ctx, database) => {
// ctx.body parsed by Express middleware
const { name } = ctx.body;
return ctx.respond.json({ message: `Hello ${name}!` });
}
}]
};
When to use Express adapter:
- Need Express-specific middleware (sessions, compression, etc.)
- Integrating with existing Express ecosystem
- Using third-party Express middleware
Default HTTP adapter is recommended for:
- Maximum performance (no framework overhead)
- Minimal dependencies
- Most common use cases (JSON APIs, SSR)
Note: Install Express if using the Express adapter: npm install express
Middleware
Clovie supports Express middleware for server applications. When you configure middleware, Clovie automatically uses the Express adapter for full compatibility with the Express middleware ecosystem.
Basic Middleware Setup
import express from 'express';
import cors from 'cors';
export default {
type: 'server',
middleware: [
express.json({ limit: '10mb' }), // Parse JSON bodies
express.urlencoded({ extended: true }), // Parse form data
cors({ origin: 'https://myapp.com' }), // CORS configuration
express.static('public') // Serve static files
]
};
Auto-Adapter Selection: Clovie automatically switches to the Express adapter when middleware is configured. If no middleware is present, it uses the faster HTTP adapter.
Authentication Middleware (Most Common Pattern)
export default {
type: 'server',
middleware: [
// Basic request logging
(req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
},
// Selective authentication - only protect certain routes
(req, res, next) => {
// Public routes that don't need authentication
const publicPaths = [
'/api/login',
'/api/register',
'/api/health',
'/api/docs'
];
// Skip auth for public routes
if (publicPaths.some(path => req.url.startsWith(path))) {
return next();
}
// Protect all /api/protected/* routes
if (req.url.startsWith('/api/protected/')) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
error: 'Authentication required',
message: 'Please provide a Bearer token'
});
}
try {
// In a real app, verify JWT token here
const user = verifyJWT(token);
req.user = user; // Attach user info to request
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid token',
message: 'Token verification failed'
});
}
} else {
// All other routes pass through
next();
}
}
],
api: [
// Public endpoint - no auth needed
{
method: 'GET',
path: '/api/login',
handler: (ctx) => ctx.respond.json({ message: 'Login endpoint' })
},
// Protected endpoint - requires Bearer token
{
method: 'GET',
path: '/api/protected/profile',
handler: (ctx) => {
// ctx.req.user is available thanks to auth middleware
return ctx.respond.json({
user: ctx.req.raw.req.user,
message: 'This is protected data'
});
}
}
]
};
Simple API Key Authentication
export default {
type: 'server',
middleware: [
(req, res, next) => {
// Only protect API routes
if (!req.url.startsWith('/api/')) {
return next();
}
const apiKey = req.headers['x-api-key'];
const validKeys = process.env.API_KEYS?.split(',') || [];
if (!apiKey || !validKeys.includes(apiKey)) {
return res.status(401).json({
error: 'Invalid API key',
message: 'Provide a valid X-API-Key header'
});
}
next();
}
]
};
Session-Based Authentication
import session from 'express-session';
export default {
type: 'server',
middleware: [
// Session middleware
session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}),
// Auth middleware using sessions
(req, res, next) => {
// Protect admin routes
if (req.url.startsWith('/admin/')) {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).json({
error: 'Access denied',
message: 'Admin access required'
});
}
}
next();
}
]
};
Testing Your Auth Middleware
# Test public endpoint (should work)
curl http://localhost:3000/api/health
# Test protected endpoint without token (should return 401)
curl http://localhost:3000/api/protected/profile
# Test protected endpoint with token (should work)
curl -H "Authorization: Bearer your-token" http://localhost:3000/api/protected/profile
# Test with API key
curl -H "X-API-Key: your-key" http://localhost:3000/api/data
Middleware Execution Order
export default {
middleware: [
cors(), // 1. CORS (must be first)
express.json(), // 2. Body parsing
requestLogger, // 3. Logging
rateLimiter, // 4. Rate limiting
authenticateUser, // 5. Authentication (after parsing, before business logic)
authorizeUser // 6. Authorization (after auth)
]
};
Middleware executes in array order. This is critical for auth flows - ensure CORS and body parsing happen before authentication.
Common Middleware Patterns
// Error handling middleware
const errorHandler = (err, req, res, next) => {
console.error('Middleware error:', err);
res.status(500).json({ error: 'Internal server error' });
};
// Request timeout middleware
const timeout = (ms) => (req, res, next) => {
const timer = setTimeout(() => {
res.status(408).json({ error: 'Request timeout' });
}, ms);
res.on('finish', () => clearTimeout(timer));
next();
};
// Custom headers middleware
const securityHeaders = (req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
};
export default {
type: 'server',
middleware: [
timeout(30000), // 30 second timeout
securityHeaders, // Security headers
express.json(),
// ... your auth middleware
errorHandler // Error handling (should be last)
]
};
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: (data) => data.posts, // Array of items to create pages for
data: (globalData, post, key) => ({
...globalData,
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.
Data Function Signature: (globalData, item, key)
globalData- The data object from your configitem- Current item from the repeat arraykey- Index or key of the current item
Output:
posts/first-post.htmlposts/second-post.htmlposts/third-post.html
Dynamic Routes (Server Mode)
export default {
type: 'server',
routes: [{
name: 'User Profile',
path: '/user/:id',
template: './views/profile.html',
data: async ({ context }) => {
const userId = context.params.id;
return {
userId,
title: `Profile - ${userId}`
};
}
}]
};
Server Mode Routes: Rendered on each request. The setup function receives { context, rerender } and returns template data.
Setup Function Signature: async ({ context, rerender })
context- Request context (params, query, body, req)rerender- Call to manually invalidate the cached render
Key Differences from Static Mode:
- No
repeatfunction - routes match dynamically - Access to request parameters and query strings
- Rendered on-demand
- Must return data via
ctx.respondmethods
Data Transformation in Routes
export default {
// ... other config
routes: [{
name: 'Products',
path: '/products/:slug',
template: 'product.html',
repeat: (data) => data.products,
data: (globalData, product, key) => ({
...globalData,
product: {
...product,
price: `$${product.price.toFixed(2)}`,
slug: product.name.toLowerCase().replace(/\s+/g, '-'),
inStock: product.quantity > 0
},
relatedProducts: globalData.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: (data) => data.posts,
data: (globalData, posts, pageInfo) => ({
...globalData,
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>{{ post.title }} - {{ title }}</title>
</head>
<body>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.excerpt }}</p>
<div>{{ post.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
viewspath in your config is correct. - Create the
viewsdirectory 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:
static- Static site with SEO, responsive design (default)server- Full-stack server application with API routes, database, and SSR