sitefile
@johnhenry/sitefile builds static sites the way a Dockerfile builds images. A Sitefile is a list of
instructions: IMPORT names a resource (a folder, a file, a URL, a Git repository, an archive, Markdown posts, YAML or
CSV data), COPY and RENDER place things in an immutable file tree, TRANSFORM runs a plugin, and EXPORT writes
the result as a directory, a zip, tar or tar.gz, a Web Bundle, or an OCI image. There is no container runtime and no
shell, and the npm package has no runtime dependencies.
# SitefileARG TITLE = "Hello"COPY ./public/ /RENDER --escape=html ./templates/index.html TO /index.htmlEXPORT / AS directorysitefile build . --build-arg TITLE="My site" # writes ./dist/Read these before the reference. Each one is a deliberate rule that a first build runs into.
- Values are not HTML-escaped unless you ask.
RENDERsubstitutes${NAME}as written. Add--escape=htmlto everyRENDERthat writes HTML; then${x|raw}is the one way to insert markup (rendered Markdown, say). - JavaScript plugins don’t run until you name them. A
PLUGIN … FROM ./x.mjsline stops the build withE_UNSUPPORTEDand the exact flag to add:--allow-plugin x. A Sitefile cannot allow its own plugins, and there is no config file or environment variable for it. Import-free WebAssembly plugins need no flag. External programs (TOOL) need--allow-exec. See Plugins. - Writing different content to an existing path fails. Two instructions that write different bytes to one path stop
the build unless the second has
--overwrite. Paths that differ only in case (/A.html,/a.html) always fail, so a build gives the same files on macOS, Windows and Linux. - The first build of a remote URL locks it. An unpinned URL is recorded in
Sitefile.lock(sha256 and size). If the content changes, later builds fail withE_LOCK_MISMATCHuntil you rerun with--update-lock. Commit the lockfile; in CI use--frozen-lockfile --strict-pins. - Remote means public HTTPS. Plain
http:and private or loopback addresses are refused, on the first request and on every redirect, unless you pass--allow-http/--allow-private-network. EXPORTcomes last, and a Web Bundle needs a base URL.EXPORT / AS wbnneedsBASE https://…(or--base-url), because every file in a bundle is stored under an absolute URL.- Sitefile’s
.wbnis a plain Web Bundle; the rest of the family gzips theirs. packfile reads Sitefile’s bundles directly (see Family), butIMPORT … AS treecannot yet read the gzip-wrapped bundles that fileable’sencode="wbn"and packfile’stoArchive()write (sitefile#141). Import their directory or zip output instead. COMPRESSIONis for OCI exports only in 0.0.0. On any other format it is a validation error. Fixed on main, in the next release:AS wbn COMPRESSION gzip(or-o site.wbn.gz) writes the gzip-wrapped bundle the rest of the family uses (sitefile#143).- The npm package is the TypeScript implementation. The Rust binary is faster and is the only one with the editor
language server (
sitefile lsp); it builds from the repository withcargo install --path rust, and prebuilt binaries are not published yet. Both produce byte-identical output.
Install
Section titled “Install”npm install -g @johnhenry/sitefilenpx @johnhenry/sitefile build . # or without installingNot yet on npm: the first publish of @johnhenry/sitefile is pending.
Node 26 or later (engines). No runtime dependencies.
Provenance: a new package. Sitefile was never published to npm under any name, and 0.0.0 is its first version.
Under npm’s caret rules ^0.0.0 matches only 0.0.0, so pin exactly until a deliberate 0.1.0.
Quick start
Section titled “Quick start”A blog from Markdown posts with frontmatter, paginated, with tag pages, a feed, a sitemap, fingerprinted CSS and a link check:
ARG SITE_URL = "https://blog.example.com"ARG DRAFTS: boolean = falseIMPORT posts FROM ./posts AS markdown SORT BY date DESC SCHEMA ./post.schema.yaml DRAFTS ${DRAFTS}COPY ./public/ /RENDER --escape=html ./templates/index.html TO /page/${page.number}/index.html FOR page IN posts PAGE 10 FIRST /index.htmlRENDER --escape=html ./templates/post.html TO /posts/${post.slug}/index.html FOR post IN postsRENDER --escape=html ./templates/tag.html TO /tags/${tag.key}/index.html FOR tag IN posts BY tagsFEED posts BASE ${SITE_URL} LINK /posts/${item.slug}/index.html TITLE "Field Notes"SITEMAP BASE ${SITE_URL}FINGERPRINT /style.css MANIFEST /asset-manifest.jsonCHECKLINKSEXPORT / AS directorysitefile build . # ./dist/sitefile build . --build-arg DRAFTS=true -o previewsitefile serve --watch . # build in memory, serve on 127.0.0.1, live reloadWhere it fits
Section titled “Where it fits”Sitefile is the build orchestrator among the family’s static tools. It runs the pipeline; its siblings write file trees in code and serve the result, and they meet at file formats rather than imports:
- fileable writes trees in JSX (scaffolding, codegen). Its output directory or zip is a Sitefile input.
- packfile reads and serves Web Bundles. It reads Sitefile’s
EXPORT … AS wbnoutput back into the exported files and serves it as a(Request) => Responsehandler.
Family has the details and a decision guide.
In this section
Section titled “In this section”- The language: every instruction, templates, stages, data imports
- Command line: build, validate, graph and why, bake, watch, images and serve, export formats
- Plugins: JavaScript and WebAssembly renderers, the sandbox, external tools
- Security model: what a build of someone else’s Sitefile can and cannot do
- Family: Sitefile, fileable and packfile together
- Sitefile and Docker: site images Docker runs, Dockerfile stages, shared registries and credentials