Simple, imperative JavaScript-based static site generator
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-29 21:46:35 -04:00
.gitignore add autoreload and install scripts 2022-03-26 10:24:44 -04:00
.npmignore improve build speed in projects with many behavior module instances, hash behavior modules for cache busting, bump version to 1.2.3 2026-10-29 21:46:35 -04:00
.prettierignore don't format README.md 2026-05-30 20:33:36 -04:00
index.mjs improve build speed in projects with many behavior module instances, hash behavior modules for cache busting, bump version to 1.2.3 2026-10-29 21:46:35 -04:00
LICENSE Initial commit 2022-03-26 00:23:28 +00:00
package-lock.json use typed prettier config, use tabs 2026-10-17 02:50:35 -04:00
package.json improve build speed in projects with many behavior module instances, hash behavior modules for cache busting, bump version to 1.2.3 2026-10-29 21:46:35 -04:00
prettier.config.mjs use typed prettier config, use tabs 2026-10-17 02:50:35 -04:00
README.md add link to the blog template 2026-10-23 21:13:16 -04:00
tsconfig.json I remembered that I wasn't using TypeScript syntax in this project 2026-10-17 03:07:52 -04:00

Wunphile

npm

Simple, imperative JavaScript-based static site generator, compatible with Node.js and Bun.

Implemented as a single file with no mandatory dependencies, Wunphile is an SSG that will still work in 5 years.

Either npm install wunphile or drop index.mjs into your project and import it.

Get started with the blog template, or read on.

About

Wunphile (pronounced "one file") allows you to build your site as if it was an Express-like app, but generate a nice, static bundle.

Below is what a simple blog site might look like:

import config from './config.js'

import { Wunphile, html } from 'wunphile'
import { getAllBlogPosts } from './util/blog.js'

import IndexPage from './component/page/IndexPage.js'
import BlogPostPage from './component/page/BlogPostPage.js'

const ssg = new Wunphile(import.meta.url)

// We use the `page` to register components for pages.
ssg.page('/index.html', IndexPage)

// Components are simply functions that return HTML using the "html" template function.
// Similarly to React, strings interpolated into the template function will be sanitized.
// The "html" template function takes in a template string and returns React-style RenderFragments.
// No JSX needed!
ssg.page('/test.html', () => html`<h1>Hello from ${config.siteName}!</h1>`)

// We can write any logic we want to drive site generation.
// The "getAllBlogPosts" function could be reading markdown files from a directory,
// pulling from a database, or even fetching data from an API.
for (const post of await getAllBlogPosts()) {
    ssg.page(`/blog/${post.slug}/index.html`, () => BlogPostPage({ post }))
}

// We mount static files from a local "assets" directory to the "/static" path.
// So if there is a file named "logo.png" in the "assets" directory, it will be served at "/static/logo.png".
ssg.staticDir('/static', './assets')

// We can also mount individual static files.
// The line below will mount the local file "./metadata-files/favicon.ico" to the "/favicon.ico" path.
ssg.staticFile('/favicon.ico', './metadata-files/favicon.ico')

// Runs the site generator CLI.
// The CLI can build the site or serve it on a local HTTP server for development.
await ssg.cli()

The above code will look very familiar to anyone who has used Express or a similar imperative-style web framework. Paths are mapped to functions that return HTML, static directories are mounted, etc.

Unlike backend frameworks, however, Wunphile generates all the HTML and files at build time. It writes everything to an output directory, ready to be served by a traditional webserver such as Nginx or Caddy.

Its goal is to live up to the following qualities:

Simple

Unlike a framework such as Astro or Next.js, this library does not perform any magic or postprocessing on the HTML. Instead, it simply acts as a wrapper around your own logic that handles all the filesystem operations for you and provides an abstraction for paths, components and static files. Because it does not do any postprocessing, what you write is what you get, even down to whitespace and indentation.

Since it does not perform any transformations on input, it does not rely on a build system or transpiler. This means that it can avoid the weight and headache of a core dependency on a tool like Webpack. Sites made with this library are likely to work many years down the road due to it using almost entirely standard JavaScript APIs, along with a handful of Node.js APIs for interacting with the filesystem.

Pure JavaScript

This library consists of a single pure JavaScript file with no mandatory1 dependencies other than Node.js APIs. Instead of using JSX, it takes advantage of the JavaScript template literal syntax to provide a simple, safe API for writing HTML.

While it is not written in TypeScript, the library is fully typed and documented using JSDoc.

Because it is written in JavaScript, it can be used in any non-browser environment, including Bun. In fact, due to it only being a single file with no external build dependencies, it can be used in projects without even needing a package manager!

1 While there are no mandatory dependencies, development mode requires some way to map file extensions to MIME types. Wunphile's NPM package includes mime-db as a dev dependency to provide MIME mappings. If you are using Wunphile without NPM, see Development Without NPM.

Static

Instead of trying to be a full-stack framework, this library is designed only with static site generation in mind. It is not a framework for building web apps, and does not provide any dynamic routing or middleware.

More complicated frontend integration is out of the scope of this library, but there is nothing stopping developers from using it as a basis for a framework that does provide such features.

Client-Side Behaviors

Wunphile supports client-side behaviors, which are JavaScript modules that run on the browser.

These behaviors can add interactivity to components in a modular way without needing to introduce a build step.

Here's an example of a behavior that add a click counter to a button:

// src/component/Counter.ts

import { type Component, html, BehaviorComponent } from 'wunphile'

type CounterProps = {
    initialCount: number
}

export const Counter: Component<CounterProps, void> = ({ initialCount }) => {
    return BehaviorComponent({ module: import('../client/behavior/Counter.ts') }, html`
        <button data-initial="${initialCount}">Click me!</button>
    `)
}
// src/client/behavior/Counter.ts

import type { BehaviorModule } from 'wunphile'

export default {
    behaviorModuleUrl: import.meta.url,
    behavior: (_) => {
        const button = _ as HTMLButtonElement
        const count = parseInt(button.dataset.initial)!

        button.addEventListener('click', () => {
            button.textContent = `Clicked ${count++} times!`
        })
    },
} satisfies BehaviorModule

The behavior module will run on the client as long as you include ${BehaviorLoader()} at the end of your page component's body. Wunphile automatically processes and injects modules into the page when they are used in a component.

Behavior modules can even import normal JavaScript modules, as long as they have no top-level statements or side effects, and are within the client directory. Modules written in TypeScript will work without a build step on Bun and Node.js 24 or later.

Development Mode and Hot Reloading

When the cli method is called with the --dev or -d option, a development server will be started.

The development server will watch the project root directory for changes and hot reload the site when changes are detected.

While in development mode, the site will not be built and the filesystem will not be touched. Instead, the site will be served from an embedded HTTP server.

Whenever the site is hot reloaded, pages open in the browser will be automatically reloaded through a small script injected into pages in development mode. This can be disabled by setting the SSG_DEV_NO_INJECT environment variable to 1.

To disable hot reloading entirely, set the SSG_DEV_NO_HOT_RELOAD environment variable to 1.

Note that whenever the site is hot reloaded, the main module will be re-run in a worker thread. This means that the main module should not contain any extra code besides what is necessary to build the site.

Development Without NPM

Development mode requires some way to map file extensions to MIME types. Normally, this is handled by Wunphile's NPM dependency on mime-db, but there are other ways to provide it.

On Linux and BSD systems, FreeDesktop's Shared MIME-Info Database can be used for MIME resolution. If you only ever intend on developing your project from a system that has this, there is nothing else you need to add.

On platforms without FreeDesktop, such as Windows and MacOS, you can save mime-db's db.json as mime-db.json in the same folder as your copy of Wunphile and it will be used instead of trying to use the NPM version of mime-db. For example, if you have Wunphile saved as wunphile/index.mjs in your project, there should also be a wunphile/mime-db.json file.

Installation

Using NPM npm install wunphile
Using PNPM pnpm add wunphile
Using Yarn yarn add wunphile
Using Bun bun add wunphile
Without a package manager To use this library without a package manager, copy the library `index.mjs` file to your project and import it.

Note that the development mode CLI is not available in this case, because required dependencies for it are not included.

Note that your project must support ESM (ES2022 modules) to import the library.

CLI Usage

If your project calls the cli method, it will act as a CLI for building and serving the site.

If run with no arguments, it will build the site.

If run with --dev or -d, it will start a development server.

See the --help option for more information about the CLI and environment variables that can be used to configure it.

Example Project

You can look at an example project that implements a simple blog site here.

IDE Integration

Wunphile uses ES6 template literals for composing HTML. Different IDEs need different configuration to provide syntax highlighting and intellisense for HTML inside template literals.

VS Code

Install the es6-string-html extension.

JetBrains IDEs

Recent versions of JetBrains IDEs should support syntax highlighting and intellisense for HTML inside template literals out of the box.

Contributing

If you have found a bug, feel free to open an issue or send a PR that fixes it.

If you're interested in adding a feature, please open an issue first so that we can discuss it. I consider Wunphile to be functionally complete, so I am unlikely to merge unsolicited PRs for new features.