The public entrance to the Jennifer programming language - the site served at www.jennifer-lang.dev, pointing on to the documentation, the deck registry, Grimoire and the source.
It is written in Jennifer, all the way down:
httpdis the server - a pull loop with a pool ofspawned workers and a drained shutdown.tenginerenders the pages from a base layout, a set of partials and one page template each.src/highlight.jcolors every code sample - the Jennifer language, highlighted by a Jennifer program.
No JavaScript framework, no CDN, no build step, and nothing fetched at run time. The two fonts are self-hosted; the JavaScript is one nine-line script that applies a stored theme before the first paint, plus progressive enhancement the site works fine without.
./bin/serve # http://127.0.0.1:8080/
./bin/serve --addr :80 # every interface, port 80
./bin/serve --dev # re-read content and templates on every request
./bin/serve --help| Flag | Default | What it does |
|---|---|---|
--addr, -a |
127.0.0.1:8080 |
host:port, :port, or unix:/path/to.sock |
--workers, -w |
4 |
accept loops to spawn; each pulls from the same queue |
--dev, -d |
off | re-render content and templates per request |
--root, -r |
the directory above bin/ |
project root, if you start it from elsewhere |
Pages are rendered once at start-up and answered from memory; public/ is
handed to the engine's static file server. SIGINT / SIGTERM are trapped, so
the server drains in-flight requests before the process exits.
bin/
serve the server (executable, shebang)
healthcheck container health probe, also Jennifer
src/
server.j routes, headers, worker pool, shutdown
content.j content/ -> one json.Value tree
redirects.j the manual's old URLs -> the docs host (301)
downloads.j /download/<alias> -> the newest release's asset (302)
render.j tengine set assembly
highlight.j Jennifer syntax highlighting (vendored, see below)
content/
site.json every word on the site, and every link
backdrop.j the program in the tilted editor behind every page
examples/*.j the code samples - real programs
examples/*.out the output each one actually produced
.github/workflows/
build.yml check with the shipped interpreter, then build and push
docker-compose.yml the whole VPS deployment
templates/
base.html the document skeleton
partials.html header, footer, code card
home.html install.html showcase.html projects.html notfound.html
public/
css/site.css the whole design, both themes
js/theme.js applies a stored theme before the first paint (blocking)
js/site.js theme switch, copy buttons, scroll reveals (deferred)
fonts/ img/ favicon.* robots.txt site.webmanifest
.well-known/security.txt
Every page lives under a language base. English is the only language so far.
| Path | What |
|---|---|
/ |
negotiates Accept-Language and 302s into a language (/en/) |
/en/ |
the landing page |
/en/install |
install instructions per platform |
/en/showcase |
all ten code samples |
/en/projects |
featured projects written in Jennifer |
/install, /showcase |
the flat URLs the site was born with - 301 to /en/... |
/index.html |
this domain's root document - 301 to /, which then negotiates |
/libraries/*.html, /modules/*.html, /user-guide/*.html, /technical/*.html, /design-stances.html, ... |
the manual, which used to live here - 301 to docs.jennifer-lang.dev |
/download, /download/<alias> |
302 to the newest release, or to that release's asset |
/healthz |
ok, for a load balancer or the container health check |
/sitemap.xml |
generated from the route table, one entry per language and page |
anything under public/ |
served by httpd.serveDir |
| anything else | the 404 page |
The manual moved off this domain. It was served from here before
docs.jennifer-lang.dev existed, so every path it published - 157 of them, plus
the directory URLs - is listed in src/redirects.j and answered with a 301.
The one exception is /index.html: it was the manual's front page, but it is
also this domain's root document, so it stays here and goes to /.
That is the status that carries a page's search standing to its new address
instead of leaving the link dead. The map is old path -> path on the docs host,
so a page that was later renamed can be re-pointed by editing its line, and the
four documentation directories are also matched by prefix, so a page the manual
grows after this snapshot moves too. Regenerate the list from the manual's own
tree (docs/**/*.md -> .html) when it changes; jennifer test src/redirects_test.j checks the shapes and that no target points back here.
Caching is content-addressed. Two things carry a build's identity. The
stylesheet and the scripts are linked with a digest of themselves
(/css/site.css?v=46badca40e), so a deploy that changes the design changes the
URL, and a reader holding an hour-old copy stops holding it - a container
restart alone cannot do that, because max-age freshness means the browser
never asks. And every answer carries an ETag over its own bytes. A page is hashed when it
is rendered at start-up and handed to httpd.etag, which sets the validator and
answers a matching If-None-Match with an empty 304; static files go out
through httpd.serveDirEtag, which hashes the file itself and caches the digest
by mtime and size. Because a tag is the content, two containers built from the
same commit agree, so a reader who lands on another replica revalidates instead
of downloading again - which the Last-Modified those files also carry cannot
do. --dev sends no-store instead, so an edit shows up on reload.
Downloads follow the releases. Every release asset carries its version in
the filename, so GitHub's own /releases/latest/download/<name> shortcut cannot
be used - the name is not stable. src/downloads.j supplies the missing
indirection: /download/windows-setup, /download/linux-amd64,
/download/deb-arm64, /download/openwrt-aarch64 and seven more resolve the
newest tag from the GitHub API, cache it for half an hour in a kv store the server opens and every worker
shares, and 302 to that
release's asset. Bare /download - and every failure, from no network to a rate
limit - lands on the releases page instead, so a download link here is never
dead, only sometimes one click longer. src/downloads_test.j pins the asset
names against what release.yml actually uploads.
Adding a language is a LANGUAGES entry in src/server.j and a translated
copy of content/site.json; nothing else has to move. Routing, hreflang, the
sitemap and Accept-Language negotiation all read that list. Internal links are
written language-free in the content (/install), and content.localize
rewrites them onto the active base (/en/install) at render time, so neither
the templates nor the content ever spell a language out.
Words and links live in content/site.json - nothing is hardcoded in a
template. Change a heading, a card, a nav entry or a URL there and restart (or
run with --dev).
Code samples are real .j files in content/examples/. They are linted and
run before they are published, and the .out file beside each one is the output
it actually produced:
jennifer lint content/examples/*.j
jennifer run content/examples/01-hello.j > content/examples/01-hello.outThe SPDX header and module docblock every file carries are stripped on the way to the page, so a sample shows only its code.
The background is content/backdrop.j - a real, lint-clean Jennifer
program - run through the same highlighter as the samples and set into an
editor window that is tilted away in 3D (.backdrop in the stylesheet). It is
aria-hidden, inert to the pointer, and sits under a scrim that keeps the text
column calm, so it reads in the margins and the gaps and nowhere else. Swap the
file and the whole background changes.
Themes. The system preference is the default: with no choice stored, the
stylesheet follows prefers-color-scheme on its own. The switch in the header
is a plain light/dark toggle, and the choice is written to a jennifer-theme
cookie (a year, SameSite=Lax), so a reader who picked one gets it back on the
next visit. theme.js re-applies it before the first paint, so there is no
flash of the wrong theme; with JavaScript off the switch is hidden and the
system preference stands. Both palettes are token sets on
:root - the light one is declared twice on purpose, once for the media query
and once for the explicit [data-theme="light"], because CSS cannot merge the
two into one rule.
Design is one stylesheet, public/css/site.css. The palette is two accents
on a near-neutral dark ground with a faint green cast: ember orange
(--ember) carries structure and every interaction - section indices, headline
accents, links, buttons, keywords - and moss green (--moss) carries the
metadata layer: stat labels, hostnames, shell prompts, program output, strings.
Everything else is hairline rules and two self-hosted variable fonts (SIL OFL
1.1): Archivo - an engineered grotesque whose width axis separates the
display role (expanded) from the text role (normal) - and Spline Sans Mono
for code, labels and navigation. Change the two accent tokens in :root and the
whole site follows; change --font-display / --font-body for another
technical sans and nothing else has to move.
docker build -t jennifer-website .
docker run --rm -p 8080:8080 jennifer-websiteThe base image is
ghcr.io/jennifer-language/jennifer:0.25,
so the image is the interpreter plus this repository - no other runtime. The
build lints every .j file with the interpreter that will run them, and the
HEALTHCHECK is itself a Jennifer program (bin/healthcheck), so the image
needs no curl or shell to probe itself.
.github/workflows/build.yml checks the site with the interpreter it ships
with - formatting, lint, and a diff proving every published transcript is still
what the sample prints - then builds and pushes a multi-arch image to
ghcr.io/<owner>/<repo> and smoke-tests it. latest follows main; every push
also gets an immutable sha-xxxxxxx tag, and a version tag publishes X.Y.Z
and X.Y.
On the server, docker-compose.yml is the whole deployment - copy that one file
across and:
docker compose pull
docker compose up -dIt publishes on 127.0.0.1:8080 only, so the host's reverse proxy owns TLS and
the public port (nginx and Caddy snippets are in the file). The container runs
with a read-only root filesystem, every capability dropped, no-new-privileges,
a 256 MB cap and rotated logs. Updating is docker compose pull && docker compose up -d; pin an exact build by putting TAG=sha-abc1234 in a .env beside the
compose file.
upstream jennifer_site { server unix:/run/jennifer/site.sock; }
server {
listen 443 ssl;
server_name www.jennifer-lang.dev;
location / {
proxy_pass http://jennifer_site;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}./bin/serve --addr unix:/run/jennifer/site.sock- Domains. Every external URL is in
content/site.jsonunderlinks. The documentation is linked asdocs.jennifer-lang.dev; if the docs keep a different hostname, that one file is the only place to change it. src/highlight.jis vendored from Grimoire (src/highlight.jthere), byte-for-byte, so a fix upstream is a straight copy back. It emits highlight.js class names (hljs-keyword,hljs-string, ...), whichpublic/css/site.cssthemes.JENNIFER.mdis the language reference for AI assistants working on the site. It is optional - the image copies*.mdwith a glob precisely so a checkout without it still builds - but committing it is worth it: an assistant pointed at it writes correct.jfrom the first line. Keep it in sync with the installed interpreter; the marker on its last line records which version it matches.- Response headers are all set in one place,
commonHeadersinsrc/server.j- the bare-engine equivalent of aweb.beforemiddleware, since every answer passes through it. It stamps the security headers (CSP,nosniff, referrer policy; the CSP is'self'-only, which is why nothing here loads from a CDN) and advertises the stack:Server: Jennifer/<version>andX-Powered-By: Jennifer httpd + tengine. Thehttpdengine sends noServerheader of its own by design - this site opts in, because it is the language's own front door.
LGPL-3.0-only, matching the language. The logo files under public/img/ are the
project's own marks; the fonts under public/fonts/ are SIL OFL 1.1.