A small, cross-platform static site generator for Scala 3 — Hugo-style, built on a stack of small libraries:
- markdown — CommonMark 0.31.2 parser
- squiggly — Hugo/Liquid-style template engine
- toml — site config (
site.toml) - scala-yaml — YAML frontmatter
- path + cross_platform — file I/O across JVM / Scala.js / Scala Native
- emoji —
:smile:→ 😄
Several ready-to-build examples ship with the source, under
docs/demos/:
sbt 'juicerJVM/run build --source docs/demos/minimal'
sbt 'juicerJVM/run build --source docs/demos/handrolled-docs'Each builds into <source>/public/. For live preview during authoring:
sbt 'juicerJVM/run serve --source docs/demos/handrolled-docs --port 8080'then open http://localhost:8080/. Each bundled theme also has a
ready-to-preview demo site at docs/demos/<theme>/ — see the
themes/<theme>/ section in the docs for the live URL and a
description of what it exercises.
juicer is a tool, so it installs as one:
brew tap edadma/tap
brew trust edadma/tap
brew install edadma/tap/juicermacOS on Apple silicon, and Linux on x86_64 and arm64; every release also
attaches the plain binary for each. libuv is the only dependency.
The asset pipeline shells out to sass and esbuild rather than vendoring
either, so a site gets the current version of both — but that pipeline runs
on the JVM only, and what installs here is the Scala Native binary, which
copies SCSS and JS through unchanged. Neither tool is worth installing for
juicer's sake alone.
Other platforms build from source:
sbt juicerNative/nativeLink # a standalone binary
sbt 'juicerJVM/run build -s <site>' # or straight from the buildThe library API — io.github.edadma.juicer.App.build(...) and
App.run(args) — is there for embedding juicer in another pipeline, but the
artifact is not published to Maven Central: nobody consumes a site generator
as a dependency, so a source dependency is the way to reach it.
mysite/
├── site.toml # site config (overlays a built-in baseline)
├── content/ # markdown sources with YAML frontmatter
│ ├── _index.md
│ └── posts/
│ └── hello-world.md
├── layouts/_default/ # squiggly templates that wrap content
│ ├── file.html # used for individual pages
│ └── folder.html # used for *_index.md* pages
├── partials/ # reusable squiggly fragments
├── shortcodes/ # [= name args =] preprocessor templates
└── static/ # copied as-is into the output
A markdown file looks like:
---
title: Hello, World
date: 2026-05-07
tags:
- intro
---
This is the body.YAML frontmatter (between --- lines) is parsed by scala-yaml; the body
goes through markdown with auto-generated heading IDs; the result is
rendered into the page's layout via squiggly.
Juicer Site Generator v0.4.5
Usage: juicer [build|config|theme|serve] [options]
━━━━━ General Options ━━━━━
-b, --baseurl <URL> base site URL
-c, --config <name> base site configuration (default is 'standard')
-h, --help prints this usage text
-v, --verbose verbose output
--version prints the version
━━━━━ Commands ━━━━━━━━━━━━
Command: build [options]
Build the site
-d, --dest <path> destination directory path
-s, --source <path> site sources directory path
-D, --drafts include draft pages (frontmatter `draft: true`)
-F, --future include future-dated pages (date frontmatter past `now`)
Command: config [options]
Show build configuration
-s, --source <path> site sources directory path
Command: theme [add|upgrade] <args>...
Theme management
Command: theme add [options] <git-url>
Install a theme from a git URL into <src>/<themeDir>/
-s, --source <path> site sources directory path
-n, --name <name> install under this theme name (default: derived from URL or --subdir)
-r, --ref <branch|tag|sha>
branch, tag, or commit to check out (default: repo HEAD)
--subdir <path> install only this subdirectory of the cloned repo as the theme
--force overwrite an existing theme directory
<git-url> HTTPS or SSH git URL of the theme repo
Command: theme upgrade [options] [<name>]
Re-fetch installed themes from their recorded source
-s, --source <path> site sources directory path
-r, --ref <branch|tag|sha>
override the recorded ref for this upgrade only
<name> theme to upgrade (default: every theme with .juicer-theme.toml metadata)
Command: serve [options]
Build and serve the site
-d, --dest <path> destination directory path
-s, --source <path> site sources directory path
--host <host> host to bind to (default 'localhost')
-p, --port <port> port to listen on (default 8080)
-D, --drafts include draft pages (frontmatter `draft: true`)
-F, --future include future-dated pages (date frontmatter past `now`)
-L, --live-reload rebuild on source changes and reload browser tabs
Every command — build, config, theme, serve — runs on the JVM and
on Scala Native, which is what the released binary is. The serve command
is built on microserve, which
provides a single static-file server / live-reload abstraction across every
runtime; live reload is long-polled rather than SSE, so a multi-page site
can't exhaust the browser's per-host connection pool.
site.toml overlays one of three built-in baselines (selected by -c).
The standard baseline (default) expects the layout shown above. The
baselines themselves are in
BaseConfigs.scala.
title = "My Site"
author = "Ed"
baseURL = "https://example.com"
# Optional: drive the sidebar / nav order. Items ending in a markdown
# extension reference a content file (path relative to content/);
# anything else is a section label.
nav = [
"Getting Started",
"_index.md",
"guide/installation.md",
"Reference",
"guide/cheatsheet.md",
]Layouts and partials use squiggly syntax: {{ .field }},
{{ for x <- .items }}, {{ if cond }}, {{ partial 'name' . }},
{{ define name }} … {{ end }}. The page-rendering context exposes:
| key | what |
|---|---|
site |
site config (TOML) plus toc and start nav data |
page |
the page's YAML frontmatter (any-data shape) |
content |
the rendered markdown body (HTML) |
toc |
the page's heading tree |
sub |
flattened sub-headings (for an "On this page" list) |
| name | what |
|---|---|
relURL '...' |
site-relative URL — prepends baseURL.path |
absURL '...' |
absolute URL — prepends baseURL.base + baseURL.path |
markdownify '...' |
render a string as markdown into HTML |
emojify '...' |
substitute :shortcode: → emoji glyph |
- Auto heading IDs — every
<h*N*>gets anidattribute slugified from its text content, suitable for permalinks. - TOC tree —
{{ for h <- .toc.headings }}walks each top-level heading; each entry haslevel,contents,id, and a nestedsubof the same shape.
Inside markdown content, the bracket-equals preprocessor expands
[= name args =] into the template at shortcodes/name.html. A self-closing
shortcode is [= name args / =]; a paired one wraps content between
[= name =] and [= /name =]. Inside the template, positional args are
exposed as args[i] and named args as key.
build.sbt cross-builds for JVM, Scala.js, and Scala Native from one set of
sources. JVM is where development happens and Scala Native is what ships;
both run the full integration suite. The Scala.js target does not link at
the moment — the syntax highlighter's per-block timeout guard uses
java.lang.Thread, which Scala.js has no implementation of.
There are no JVM-only features — serve and its live-reload watcher both
use microserve, which abstracts over
java.nio (JVM), Node net/fs.watch (JS), and libuv (Native) behind a
shared Scala API. What is JVM-only is the shelling-out: the Sass/esbuild
asset pipeline and the image-variant encoder run real commands on the JVM
and degrade to a verbatim copy elsewhere, so a site built by the released
binary still resolves every asset URL.
sbt juicerJVM/test
sbt juicerNative/testsbt juicerJS/test is the third of these and cannot run while the JS target
fails to link (see Cross-platform above).
Almost every test is an end-to-end build: the suite writes a small site into
a temp directory, runs the real build pipeline over it, and asserts the
rendered HTML. Suites are named for the feature they cover
(PermalinksSpec, TaxonomiesSpec, I18nSpec, ...); JuicerBuildSpec is
the original catch-all.
