Generate nice art for your blog posts.

Files
README.md

blogart #

Seed-reproducible abstract art for blog article covers. One command, one PNG, one seed you can record in your front-matter and replay later for a byte-identical image. The same binary also serves a local web gallery that learns what you keep — see The web gallery below.

Install #

Requires Go 1.27+. Build from a checkout of this repo:

go build -o blogart ./cmd/blogart

Or a static, stripped release binary (the committed web/dist is embedded, so no node toolchain is needed either way):

just release          # current OS, into out/blogart
just release linux    # or: darwin, windows...

Then put blogart somewhere on your PATH. From there, one command makes a cover:

$ blogart cover.png

Jump to Usage for all flags, The web gallery for the interactive mode, or keep reading for the reproducibility idea in one glance:

$ blogart cover.png
seed pwbb7qll · style waves+julia · palette sand · 4800x2520
reproduce: blogart -seed pwbb7qll cover.png

$ blogart cover2.png
seed 00000007 · style blobs+strings · palette ocean · 4800x2520 · symmetry 3
reproduce: blogart -seed 00000007 cover2.png

Every run without a -seed picks a fresh random seed, and that seed picks everything else — the style (or pair of styles), the palette, and whether the cover folds into a kaleidoscope — so articles get varied covers with zero thought. Pass the seed back and the same binary reproduces the same image, byte for byte. Output depends only on (seed, style, palette, width, height, symmetry) for a given binary version.

The default experience: mixing and symmetry #

Plain single-style covers are only half the story:

  • Style mixing. About half of runs composite two styles: the background style renders the full canvas, the foreground style renders an offscreen layer of the same size, and the two are alpha-blended at an opacity the seed draws (roughly 25–70%). The seed picks the pair, the order, and the opacity — any pair is possible, including a style with itself. The summary shows the composite background-first (style waves+julia).
  • Radial symmetry. About one run in three folds the composed image into an N-fold radial mirror around the center (a mandala/kaleidoscope look), with N drawn from {2, 3, 4, 5, 6, 8}. The summary appends · symmetry N.

Both decisions live in the seed, so the seed alone reproduces them — a mixed, folded cover regenerates from nothing but its seed. Pass -style to pin exactly one style (which also disables mixing) or -symmetry to control the fold; see below.

Usage #

blogart [flags] <output.png>
Flag Default Meaning
-seed random each run 8-character lowercase base36 seed to replay a render
-style chosen by the seed art style name; pins one style and disables mixing
-palette chosen by the seed color palette name (see below)
-symmetry chosen by the seed fold count N (≥ 2), or off to force no fold
-w 4800 image width in pixels
-h 2520 image height in pixels

The default 4800x2520 is 4x the Open Graph landscape size (sharp on retina/hi-DPI), so hero covers need no flags. Square inline figures: -w 900 -h 900.

-symmetry N overrides the seed's fold decision and count — useful when you want exact geometry:

$ blogart -seed 0flvkxbp -symmetry 5 cover.png
seed 0flvkxbp · style waves · palette autumn · 4800x2520 · symmetry 5
reproduce: blogart -seed 0flvkxbp -symmetry 5 cover.png

-symmetry off (0 is accepted as a synonym) forces no folding even on a seed that would fold. -symmetry 1 and negative values are mistakes, not choices, and fail with an error. Pinning -style does not suppress the seed's symmetry — pinning controls composition, not geometry.

The output path must end in .png — anything else is rejected with an error. Overwriting an existing file just works: regenerating a cover in place is the normal workflow. The tool is fully offline, and so is the web gallery: it binds loopback only and bundles every asset it serves.

The seed workflow for blog front-matter #

  1. Generate a cover with one command:

    $ blogart posts/my-article/cover.png
    seed 00000007 · style blobs+strings · palette ocean · 4800x2520 · symmetry 3
    reproduce: blogart -seed 00000007 posts/my-article/cover.png
    
  2. Record the seed in your article's front-matter, e.g.:

    ---
    title: Notes on Seeded Art
    cover: posts/my-article/cover.png
    coverSeed: 00000007
    ---
    

    The seed alone is enough — it encodes the style pair, the blend, the fold, and the palette. Record the other flags only if you passed them.

  3. Regenerate any time — a lost file, a CI rebuild, a new machine — by running the printed reproduction line (the copy-pasteable command from the original run, or reconstructed from the front-matter):

    $ blogart -seed 00000007 posts/my-article/cover.png
    

    The regenerated file is byte-identical to the original, so a published cover never silently changes.

The reproduction line carries only what the seed cannot reproduce: the seed itself, the -style/-palette/-symmetry flags you actually passed, and -w/-h whenever the dimensions aren't the default. Copy it verbatim and it works:

$ blogart -style waves -palette ocean -symmetry 6 cover.png
seed 9avegutt · style waves · palette ocean · 4800x2520 · symmetry 6
reproduce: blogart -seed 9avegutt -style waves -palette ocean -symmetry 6 cover.png

Hunting for a cover one CLI run at a time — eyeball, rerun, note the seed — is exactly what the web mode replaces. One command opens a local gallery in your browser:

$ blogart --web
serving on http://127.0.0.1:8080 — Ctrl-C to stop
picks log: ~/.local/state/blogart/picks.jsonl

The page opens in a dark theme (a header toggle flips to light — the choice is remembered, and a machine set to light mode is respected on first visit). The first batch of covers (default 10) renders automatically when the page loads. Each cover is a framed card with its configuration as labeled badges — seed, style, palette, symmetry, size — and the seed sits in monospace with a copy button, so any image you like can later be replayed from the CLI:

$ blogart -seed pwbb7qll -style waves -palette sand cover.png

The loop #

  • Select. Click cards to mark favorites — the click order is the rank, shown as a rank badge on the card, and any number can be selected. Click again to deselect.
  • Generate. The page's one primary button. Replaces the grid with a fresh batch (skeletons while it renders) whose style and palette sampling is weighted toward your selections and past downloads. The visual selection clears; its weight stays, so bias accumulates round by round. A fixed 25% exploration floor keeps fully random images in every batch, so the gallery never collapses into a narrow rut.
  • Knobs. Batch size, the style pool, the palette pool, and the canvas dimensions (default 4800×2520) live in a sidebar pinned beside the grid (a toolbar above it on narrow windows) — adjust and regenerate without scrolling back to the top. Empty or invalid dimension fields mean "the server's default".
  • Persistence. On the next launch, the picks log re-seeds the weights with a 30-day recency half-life, so your taste profile accumulates across sittings.

Save is the pick #

Each image has a Save button: the browser downloads the PNG as <seed>.png — the download is the only copy; the server never stores images — and the server appends one line to the picks log. Downloads are the only durable signal: selections shape the live session but are never written to disk, and right-click "save image as" is deliberately not counted. Downloading the same image again appends a second line — repetition is a legitimate stronger signal.

Bias steers style and palette only: every image still gets a fresh random seed and a seed-driven symmetry. Each image is exactly reproducible from its config; the batches are deliberately not. That asymmetry is the feature — determinism at the artifact level, exploration at the curation level.

Web-mode flags #

Flag Default Meaning
-web off serve the local gallery instead of rendering one image
-port 8080 port the gallery listens on — always loopback-only (127.0.0.1)
-picks state dir picks-log path (see below)
-w, -h 4800, 2520 default batch dimensions for the page

Web mode takes no positional arguments — passing one fails with a usage diagnostic and a non-zero exit. An unwritable picks log is also a launch failure: a gallery whose Save clicks could never be remembered refuses to open. Ctrl-C (or SIGTERM) shuts the server down gracefully; every pick already appended is durable, so a crash or closed tab loses nothing.

The picks log #

The picks log is an append-only JSONL file, one object per download:

{"ts":"2026-10-16T18:30:00+02:00","session":"k3x7hq2v","seed":"pwbb7qll","style":"waves","palette":"sand","symmetry":0,"w":4800,"h":2520}
Field Meaning
ts download time, RFC 3339
session 8-character id of the server run (fresh per launch)
seed the image's seed — enough to replay it
style style name (the web layer pins one style per image, like the CLI's -style)
palette palette name
symmetry fold count, 0 means no fold
w, h canvas dimensions

The default location is $XDG_STATE_HOME/blogart/picks.jsonl, falling back to ~/.local/state/blogart/picks.jsonl — the OS user state directory, not the project dir, so working directories stay clean and your taste profile is machine-global. -picks relocates it (e.g. into a synced directory). Lines are flushed per event and never rewritten; at startup each line re-weights its style and palette, and a malformed line (a hand-edited log) is skipped, never fatal. The log is input for a future de-randomizer, not a learned model itself — the web layer only re-weights its sampling from it.

Styles #

Name Look
waves layered flowing wave bands
geo-grid geometric grid compositions
blobs soft organic radial-gradient blobs
gradient-mesh smooth multi-point color mesh
tiles tiled mosaic patterns
rings concentric ring compositions
strings thread/string art line patterns
dots dot-field compositions
domain-warp warped gradient noise field (vendored)
silk-sky smooth layered silk-like sky (vendored)
julia Julia-set fractal in palette colors (vendored)
random-walk random-walk line traces (vendored)

Every pair of styles can be mixed, including a style with itself — there is no exclusion table, and no can't mix those error.

The four vendored styles are adapted from jdxyw/generativeart (MIT); they live in internal/generativeart with upstream attribution and consume the same seeded RNG as the own-engine styles.

Palettes #

Name Feel
sunset dusk: slate blue, violet, rose, peach
ocean depth: navy, steel blue, teal, pale aqua
forest pine to sage greens with a warm gold accent
berry wine, purple, magenta, blush
sand desert earth: teal, ochre, terracotta
autumn bark, rust, burnt orange, amber, olive
neon cyberpunk night: indigo, electric purple, magenta, cyan
mono ink-on-paper grayscale ramp
flexoki warm paper, black ink, and eight muted print accents — the Flexoki scheme by Steph Ango (MIT)

A palette is a color pool: the seeded RNG draws individual colors from it, so the same palette still yields visibly different art across seeds while keeping your blog's look consistent. Both layers of a mixed render share the one selected palette, so composites stay color-coherent.

Unknown style or palette names, and invalid -symmetry values, fail with an error listing the valid options, so typos are one-glance fixes:

$ blogart -style cubism cover.png
unknown style "cubism" (valid styles: blobs, domain-warp, dots, geo-grid, gradient-mesh, julia, random-walk, rings, silk-sky, strings, tiles, waves)
$ blogart -symmetry 1 cover.png
-symmetry must be "off" or an integer N >= 2, got "1"

Seed stability #

Seeds are stable per binary version: the same seed, flags, and binary reproduce the same bytes. Improving a style in a future release may change a seed's output — re-record the seed (or commit the generated PNG) when you upgrade.

Heads-up when upgrading from the first version: mixing and symmetry became part of the default experience, so roughly half of existing seeds now render differently (remixed, refolded, or both). Re-record those seeds — or simply commit the PNGs they already produced.

Development #

just test-unit   # fast test suite through the two test seams (under 10s; deep race run: just test-race)
just lint-go     # go vet + golangci-lint
just web-build   # rebuild web/dist, the committed SPA bundle (needs node)

Tests exercise the two test seams: the CLI entry function (Run(args, stdout, stderr)) for the render path, flag/error contracts, and announcements, and the web layer's exported Server (an http.Handler in internal/web) via real HTTP roundtrips for status codes, headers, JSON shapes, and PNG bytes. web/e2e.mjs is an optional real-browser end-to-end check of the gallery loop (Firefox via geckodriver plus node — run it against a built, running binary; the unit suite never needs either). The frontend source lives in web/; its built web/dist is committed and embedded into the binary, so plain go build, go test, and CI need no node toolchain.

The specs and ticket history live in backlog/. MIT licensed — see LICENSE.