A tiny static site generator using EJS templates. No complex build pipelines, no magic. Just pages, partials, and static assets.
npm create sitemill my-site
cd my-site
npm install
npm startYou now have a working site at http://localhost:8080 with pages, a blog, and styles ready to customize.
Create a project structure:
my-site/
├── config.js
├── pages/
│ └── index.ejs
├── partials/
│ └── header.ejs
└── static/
└── css/
└── style.css
Add your site config:
// config.js
module.exports = {
title: 'My Site',
};Create a page:
<!-- pages/index.ejs -->
<%- include(partials + 'header.ejs', { config }) %>
<h1>Welcome to <%= config.title %></h1>Create a partial:
<!-- partials/header.ejs -->
<!doctype html>
<html>
<head>
<title><%= config.title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>Install sitemill:
npm install sitemillBuild your site:
npx sitemill buildYour site is now in the dist/ folder.
sitemill expects this layout:
your-site/
├── config.js # Site configuration (optional)
├── pages/ # EJS templates that become HTML pages
│ ├── index.ejs # becomes /index.html
│ ├── about.ejs # becomes /about.html
│ └── blog/
│ └── post.ejs # becomes /blog/post.html
├── partials/ # Reusable template fragments
│ ├── header.ejs
│ └── footer.ejs
├── static/ # Copied directly to output
│ ├── css/
│ ├── js/
│ └── img/
└── dist/ # Generated output (do not edit)
All paths are configurable if you need something different.
Build your site:
npx sitemill buildBuild and start a development server:
npx sitemill serveThe server runs on port 8080 by default. Set the PORT environment variable to change it:
PORT=3000 npx sitemill serveFor convenience, add these scripts to your project:
{
"scripts": {
"build": "sitemill build",
"start": "sitemill serve",
"dev": "sitemill serve & node --watch-path=pages --watch-path=partials --watch-path=static --watch-path=config.js -e 'require(\"sitemill\").build(require(\"./config\"))'"
},
"dependencies": {
"sitemill": "^0.1.0"
}
}The dev script watches for file changes and rebuilds automatically.
Add metadata to any page using a JSON comment at the start of the file:
<%#
{
"title": "About Us",
"description": "Learn more about our company"
}
%>
<!doctype html>
<html>
<head>
<title><%= pageConfig.title %></title>
<meta name="description" content="<%= pageConfig.description %>">
</head>
<body>
<h1><%= pageConfig.title %></h1>
</body>
</html>The frontmatter is parsed and available as pageConfig in your template.
Every template has access to these variables:
| Variable | Description |
|---|---|
config |
Your site configuration from config.js |
pageConfig |
Current page frontmatter plus generated fields |
pageConfig.link |
URL path to this page (e.g., /about.html) |
pageConfig.type |
Page type: page or blog |
pageConfig.title |
Page title (falls back to config.title) |
blogs |
Array of all blog posts, sorted by date |
partials |
Path to partials directory (for includes) |
pages |
Path to pages directory |
Use EJS includes to share common markup:
<%- include(partials + 'header.ejs', { config, pageConfig }) %>
<main>
<h1><%= pageConfig.title %></h1>
<p>Page content here.</p>
</main>
<%- include(partials + 'footer.ejs') %>Pass any variables you need to the partial as the second argument.
To create a blog, add pages with type: "blog" in their frontmatter:
<%#
{
"type": "blog",
"date": "2025-01-15",
"title": "My First Post",
"description": "An introduction to the blog"
}
%>
<!doctype html>
<html>
<head><title><%= pageConfig.title %></title></head>
<body>
<article>
<h1><%= pageConfig.title %></h1>
<time><%= pageConfig.date %></time>
<p>Post content goes here.</p>
</article>
</body>
</html>All blog posts are collected into the blogs array, sorted by date (newest first). Use this to create a blog index:
<h1>Blog</h1>
<ul>
<% blogs.forEach(post => { %>
<li>
<a href="<%= post.config.link %>"><%= post.config.title %></a>
<time><%= post.config.date %></time>
</li>
<% }) %>
</ul>Use type: "blog_paused" for posts you want to build but exclude from the blogs array:
<%#
{
"type": "blog_paused",
"date": "2025-02-01",
"title": "Work in Progress"
}
%>The page will be generated, but it won't appear in blog listings.
You can also use sitemill as a library:
const sitemill = require('sitemill');
// Build with default options
sitemill.build({ title: 'My Site' });
// Build with custom paths
sitemill.build({ title: 'My Site' }, {
pagesDir: 'src/pages',
partialsDir: 'src/partials',
staticDir: 'public',
outDir: 'build',
});
// Build and serve
sitemill.serve({ title: 'My Site' }, { port: 3000 });| Option | Default | Description |
|---|---|---|
pagesDir |
pages |
Directory containing EJS page templates |
partialsDir |
partials |
Directory containing partial templates |
staticDir |
static |
Directory of static assets to copy |
outDir |
dist |
Output directory for generated site |
cwd |
process.cwd() |
Base directory for resolving paths |
port |
8080 |
Port for development server |
The dist/ folder contains plain HTML, CSS, and JavaScript. Deploy it anywhere:
- Netlify: Set build command to
npx sitemill buildand publish directory todist - Vercel: Same as Netlify
- GitHub Pages: Push the
dist/folder or use GitHub Actions - Any static host: Just upload the
dist/folder
MIT