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 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:

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

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

Available Templates:

  • static - Static site with SEO, responsive design (default)
  • 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

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 };
    }
  }]
};

Back to top

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 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

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 config
  • item - Current item from the repeat array
  • key - Index or key of the current item

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 ({ 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 repeat function - routes match dynamically
  • Access to request parameters and query strings
  • Rendered on-demand
  • Must return data via ctx.respond methods

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:

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