Visitar URL original
mothwing

mothwing

Write HTML, ship HTML. A small static site builder for pages as light as a moth's wing, with hashed asset links and modulepreloads for code split JS. The rest is there when you ask for it: SEO tags, a Content-Security-Policy that allows only your own inline scripts, sitemaps dated from git, robots.txt and minified HTML.

It was born as the build script of ffxiv.watch, and got tweaked over time until I was a little too proud of it to keep it there. So it branched out into its own thing, for any site that wants the same.

What it isn't

mothwing isn't a framework. There are no components, and nothing runs in the browser that you didn't write. If you want those, Astro and SvelteKit are great.

I did try them, and others, before making my own. They're good at what they do, but I wanted full control of the output: every byte of HTML exactly as I wrote it, and as few dependencies as possible between my files and the finished pages. mothwing is for when you want the same.

Getting started

npm install --save-dev mothwing
npx mothwing dev     # dev server on http://localhost:8080
npx mothwing build   # production build into dist/

The smallest site is one file:

src/pages/index.html

The build mirrors src/, so src/images/logo.png becomes /images/logo.png. Add a config when you need SEO tags, sitemaps or anything else:

// mothwing.config.js
export default { domain: "example.com" }

The rest of the folders are there when you want them:

mothwing.config.js
src/
  pages/           every page, the folders are the URLs: about.html -> /about/
  partials/        bits of HTML the pages share
  data/            JSON and JS the pages are filled from
  js/              bundled by esbuild
  ...              everything else is copied as it is

The folder names are only defaults. pages, partials and data can each be renamed in the config. defineConfig gives your editor types so every option autocompletes and a typo is underlined:

import { defineConfig } from "mothwing"

export default defineConfig({
  domain: "example.com",
  js: { scripts: ["js/index.js"] },
  exclude: ["js"]
})

Without the import, a JSDoc comment does the same:

/** @type {import("mothwing").Config} */
export default { ... }

An option mothwing doesn't know fails the build either way.

A page is plain HTML. mothwing builds it as-is:

<!-- src/pages/index.html -->
<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>Home</title></head>
  <body><h1>Hello</h1></body>
</html>

mothwing's tags add more when you need them: partials, hashed asset links, data placeholders, and with a domain set, SEO tags:

<!-- src/pages/index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <x-seo title="Home" description="What the site is about"/>
    <x-css src="/css/index.css"/>
  </head>
  <body>
    <x-html src="partials/nav.html"/>
    <h1>{{ site.name }}</h1>
    <x-js src="/js/index.js"/>
  </body>
</html>

{{ site.name }} comes from src/data/site.json, see Data, and the tags are in Tags. In dev, a URL with no page gets your 404.html.

build checks what it made before it's done. An href or src written as a path, like /about/ or ../images/logo.png, that leads to nothing in the build fails it, with the page it's on. Full URLs are left alone, as anything can serve them. A partial no page uses gets a warning, so leftovers don't pile up.

Every option and function is in the API reference, read from mothwing's own source as this site builds.