Clovie

A Node.js-based framework for building static sites and web applications. Simple but deep, easy to start with room to grow.

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:

Two Operating Modes

Static Mode (type: 'static'): Traditional static site generation

Server Mode (type: 'server'): Full Express applications with SSR

Core Features

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

Available Templates:

  • default - Basic static site with minimal setup
  • static - Full-featured static site with navigation and multiple pages
  • server - Full-stack server application with API routes, database, and SSR

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

Back to top

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 templates
  • scripts/main.js for JavaScript entry point
  • styles/main.scss for SCSS entry point
  • assets/ 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 };
    }
  }]
};

Back to top

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 response
  • ctx.respond.html(html, status) - Send HTML response
  • ctx.respond.text(text, status) - Send plain text

Context Properties:

  • ctx.req - Request object
  • ctx.params - Route parameters (e.g., :id)
  • ctx.query - Query string parameters
  • ctx.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 endpoint
  • GET /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:

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 templating
  • pug - 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 templates
  • register(name, template) - Required function to register partials/includes

Back to top

Error Handling & Best Practices

Error Handling

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

  1. Use partial templates (files starting with _) for reusable components.
  2. Validate data structures before passing to routes.
  3. Handle async data with proper error catching in route data functions.
  4. Use meaningful route paths for SEO and organization.
  5. Transform data in route data functions, not in templates.
  6. Separate static and dynamic routes for better performance.

Back to top

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

Back to top

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

Back to top

Troubleshooting

Common Issues

"Views directory does not exist"

"Route repeat function must return an array"

"Maximum directory depth exceeded"

Build failures

Back to top

License

MIT

Back to top