Flux — Design Sandbox

Exploring typography, rhythm, and stream design for vincent.demeester.fr

§§Typography Specimen

Body — Serif (Iowan Old Style / Palatino)

The quick brown fox jumps over the lazy dog. 0123456789

The quick brown fox jumps over the lazy dog. — italic

The quick brown fox jumps over the lazy dog. — bold

Headings — Sans (Seravek / Gill Sans)

The quick brown fox jumps over the lazy dog. 0123456789

The quick brown fox jumps over the lazy dog. — semibold

Code — Mono (Cascadia Code / Source Code Pro)

func main() { fmt.Println("Hello, flux!") } // 0xDEAD

§Heading Sizes

H1 — Page Title (2rem)

§H2 — Section Heading (1.4rem)

§H3 — Subsection (1.15rem, italic)

Body text at 18px base size, 1.6 line-height = 28.8px rhythm unit.

§Dropcap

Gardens can be very personal and full of whimsy or a garden can be a source of food and substance. This is my personal space on the World Wide Web. It is meant to be simple, modest and persistent — by persistent, it means that I am trying to not break URIs. The list below is a "selection" of some content.

§Small Caps

This website uses Tufte CSS as its foundation, taking cues from Edward Tufte's principles of data-ink ratio and information density. The HTML is semantic and the CSS is minimal.

Links are styled following Adactio's guidance: subtle underline offset, thin decoration, translucent color. Hover reveals full underline. Compare:

§§Vertical Rhythm

All spacing derives from --lh (one line-height unit = 28.8px at 18px/1.6). Following Pawel Grzybek's approach with lh units, and Christian Tietze's CSS custom properties for consistency.

Paragraph spacing: 1lh (one line). Heading top margin: 2lh. This keeps text on a consistent baseline grid, which your eye perceives as calm and ordered even without consciously noticing it.

Butterick says: optimal line spacing is 120–145% of point size. We use 160% (1.6) — generous but not loose, good for long-form reading on screens.

§§Block Elements

§Blockquote — Standard

While not everybody has or works in a dirt garden, we all share a familiarity with the idea of what a garden is. A garden is usually a place where things grow.

Joel Hooks

§Blockquote — Epigraph

“The phrase “digital garden” is a metaphor for thinking about writing and creating that focuses less on the resulting “content”, and more on the process, care, and craft it takes to get there.”

Joel Hooks

§Blockquote — Pullquote

If everything is highlighted, nothing is highlighted.

Nikita Tonsky

§Blockquote — Nested / Multi-paragraph

Minimalism helps one focus on the content. Anything besides the content is distraction and not design.

‘Attention!’, as Ikkyu would say.

Gwern

§Code — Tonsky-style Syntax Highlighting

Following Tonsky’s principles: only 4 semantic categories get color. Strings (green bg), comments (warm bg), definitions (blue bg), and constants (purple text). Everything else stays default. No bold.

§Go

// Entry represents a single item in the flux stream.
// It can be a GitHub PR, a bookmark, a note, etc.
type Entry struct {
    ID     string    `json:"id"`
    Kind   string    `json:"kind"`
    Title  string    `json:"title"`
    URL    string    `json:"url"`
    Date   time.Time `json:"date"`
    Tags   []string  `json:"tags"`
    Body   string    `json:"body,omitempty"`
}

func NewEntry(kind string, title string, url string) *Entry {
    return &Entry{
        ID:    generateID(kind, url),
        Kind:  kind,
        Title: title,
        URL:   url,
        Date:  time.Now(),
    }
}

const (
    KindGitHubPR    = "github-pr"
    KindBookmark    = "bookmark"
    MaxRetries      = 3
    DefaultTimeout  = 30 * time.Second
    EnableCache     = true
)

§Python

import json
from pathlib import Path
from datetime import datetime

# Tonsky: comments deserve bright color.
# Good comments ADD to code.

class FluxAggregator:
    """Merge entries from all sources into a single feed."""

    def __init__(self, cache_dir: Path, max_entries: int = 500):
        self.cache_dir = cache_dir
        self.max_entries = max_entries
        self.sources: list[Source] = []
        self._last_run: datetime | None = None

    def generate(self) -> list[dict]:
        """Fetch from all sources, merge, deduplicate, sort."""
        entries = []
        for source in self.sources:
            new = source.fetch(since=self._last_run)
            entries.extend(new)

        # Sort reverse-chronological, deduplicate by ID
        seen = set()
        result = []
        for e in sorted(entries, key=lambda x: x["date"], reverse=True):
            if e["id"] not in seen:
                seen.add(e["id"])
                result.append(e)

        return result[:500]

§Nix

{ pkgs, lib, config, ... }:

let
  # Build the flux binary from source
  flux = pkgs.buildGoModule rec {
    pname = "flux";
    version = "0.1.0";
    src = ../.;
    vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };
in {
  systemd.services.flux-generate = {
    description = "Generate flux stream";
    serviceConfig = {
      Type = "oneshot";
      ExecStart = "${flux}/bin/flux generate";
      User = "www";
    };
  };

  systemd.timers.flux-generate = {
    wantedBy = [ "timers.target" ];
    timerConfig.OnCalendar = "hourly";
    timerConfig.Persistent = true;
  };
}
Tonsky’s 4-color rule

Notice how scannable the code becomes. Your eye immediately finds strings, comments, definitions, and constants. Keywords like func, if, return are just code — you never search for a keyword, you search for the name after it.

§Callouts

Note

A general note or annotation. Use for supplementary information the reader might find useful but isn’t essential to the main flow.

Tip

Use flux generate --dry-run to preview what entries would be added without writing any files. Handy when adding a new source.

Info

The JSON Feed spec v1.1 allows an _extension namespace for custom fields. We use _flux to store entry kind and source metadata.

Warning

GitHub’s search API is limited to 30 requests/minute even with authentication. The cache mechanism avoids hitting this on every run, but a full re-fetch (flux cache clear) will be slow.

Danger

Never commit your GITHUB_TOKEN to the repository. Use environment variables or a secrets manager. The token has read access to all your public activity.

Design Note

This callout style is borrowed from Crafting Interpreters. Each type has a left border, subtle background wash, and icon prefix. Colors follow the same CSS variable system — they adapt to light and dark mode automatically.

§Table

Reference Key Takeaway Applied Here
Gwern Aesthetically-pleasing minimalism Grayscale palette, dropcaps
Tufte CSS Margins are for thinking Sidenotes in margin
Crafting Interpreters Book-quality web typography Serif body, § anchors
Miessler Warm parchment, premium fonts Color palette
Tietze CSS custom props for rhythm --lh system
Larlet Seasonal list markers body:has(time) trick

§Figures & Images

§Standard figure with caption

A landscape placeholder showing layout elements
Figure 1. A standard figure occupies the full content width. The caption uses the heading font, italic, with a bold label prefix. Good for diagrams, charts, and photographs.

§Margin figure (Tufte-style)

A small square placeholder
Fig. 2. A margin figure floats in the right margin alongside the text, just like sidenotes. On mobile, it becomes full-width.

This paragraph has a margin figure floating next to it. The margin figure is a Tufte signature — small supporting images placed in the margin where the reader’s eye can find them without breaking the reading flow. They share the same space as sidenotes, so you should avoid placing both on the same paragraph.

The margin figure works especially well for small diagrams, icons, logos, or thumbnail images that support but don’t dominate the text. On narrow screens, it collapses inline.

§Bordered figure (framed)

A framed placeholder
Figure 3. A bordered figure adds a subtle frame — useful for screenshots or images that blend into the background without a border.

§Screenshot (drop shadow)

A screenshot-style placeholder
Figure 4. Screenshots get a drop shadow and rounded corners, making them feel like floating windows. The shadow adapts to light/dark mode.

§Full-width figure

A full-width placeholder
Figure 5. A full-width figure stretches across the content column and the margin area. Good for panoramic images, wide diagrams, or data visualizations that need horizontal space.

§Figure grid — 2 columns

Grid image 1
Before
Grid image 2
After

§Figure grid — 3 columns

Grid image A
v1.0
Grid image B
v2.0
Grid image C
v3.0 (current)

§No caption (bare image)

An image without a caption
Design Note

Org-mode exports images as <figure> + <figcaption> via #+CAPTION:. The margin figure maps to Tufte’s #+ATTR_HTML: :class margin. The grid layout would need manual HTML or a Soupault transform.

§Lists — Marker Options

Pick a list marker style. Current is ✧ (white star).

§Alternatives

  1. Ordered lists stay numbered
  2. Nothing fancy here
  3. Just clean spacing

§§Tufte Sidenotes

Edward Tufte's distinctive style places supplementary information in the margin rather than in footnotes at the bottom of the page. This is a numbered sidenote. On wide screens, it floats in the right margin. On narrow screens, it becomes an inline block below the paragraph. The approach follows Gwern's sidenote analysis. This keeps the reader's eye on the page instead of bouncing back and forth. The idea is simple: if something is worth saying, say it nearby.

Margin notes are similar but unnumbered — useful for brief asides or contextual links.This is an unnumbered margin note. Kenneth Reitz calls this "margins are for thinking." These work beautifully with Tufte CSS and ox-tufte. They provide a visual rhythm to the page, filling what would otherwise be empty whitespace with relevant context.

A paragraph without sidenotes, for contrast. Notice how the text column stays at a comfortable reading width of about 640px (roughly 65 characters per line), which typographers consider optimal. Robert Bringhurst recommends 45–75 characters per line. We aim for ~65. This is also what Gwern targets. The margin area is a bonus, not a crutch.

§§Color Palette

Light mode draws from Daniel Miessler's warm parchment. Dark mode from Beat's stream. Both shift the same CSS variables.

§Accent Color Picker

Click a swatch to preview it as the accent color across the whole page — dropcaps, blockquote borders, callout labels, pullquotes, release cards, sidenote numbers, and § anchors all update live.

Greens — Deep
Greens — Mid
Greens — Soft
Teal & Cyan
Blues — Cool
Blues — Warm
Warm Earth — Gold & Amber
Warm Earth — Brown & Umber
Orange & Terracotta
Reds — Deep
Reds — Soft & Rose
Purples
Neutrals & Greys

Current accent: #4a7c59

§Light Mode

bg
bg-alt
card
text
muted
faint
accent
link
new
updated

§Dark Mode

bg
bg-alt
card
text
muted
faint
accent
link
new
updated
Solarized Alternative

Christian Tietze uses Solarized colors: #002b36 dark, #fdf5e6 cream, #268bd2 blue. Could be an alternative palette — more muted, less warm than the Miessler-inspired one above.


Flux

What I'm working on, reading, and thinking about.

2026
tektoncd/pipeline v1.11.0 · March 30, 2026
§
Nix flakes and reproducible Go builds

Today I spent some time figuring out how to pin Go module dependencies inside a Nix flake. The trick is using buildGoModule with vendorHash — it fetches and vendors dependencies deterministically.

The NixOS wiki Go page is helpful but incomplete. Had to read the nixpkgs source to understand proxyVendor.

+ Screen Freezes on Framework 16 with AMD and GNOME Mar 26 ~ Setting Up A Repo In Under 5 Minutes With Nix Mar 25
§
selfhostblocks

Found selfhostblocks while browsing the Clan gitea. An interesting alternative approach to NixOS service modules — more opinionated, batteries-included.

Though I'm pretty convinced by Clan so far.

· GitHub
merged
merged
opened
· Bookmarks
Interesting approach to base images, similar philosophy to distroless but with better reproducibility.
stagex.tools
The essay that keeps coming back: general methods that leverage computation win over domain-specific engineering.
incompleteideas.net
+ Domain Name System Mar 15 ~ (Doom) Emacs Config Bits Mar 14 ~ Window Switching Made Simple Mar 12
§
Using dig

Just had a small DNS dispute again, and remembered the post by Julia Evans on using dig. Pretty helpful!

She also wrote a simple DNS lookup tool.

tektoncd/triggers v0.30.0 · March 5, 2026
· GitHub
NixOS/nixpkgs#298412
merged
merged
§
Bun ❤️ SQLite

Today I learned that Bun has a SQLite driver built in. That's pretty cool:

import { Database } from 'bun:sqlite'

const db = new Database('mydb.sqlite', { create: true })

const query = db.query(`
  SELECT * FROM users WHERE name = ?
`)

console.log(query.all('vincent'))
Earlier
+ A Digital Garden 2024 + Random thoughts after 2 years 2022

§§Prose Example

This section demonstrates how a full article or journal entry would look with this design system. The warm background, serif body text, and generous line-height create a reading experience closer to a book than a typical blog. Every decision — from the font choice to the margin width — serves readability.

As I said in "Random thoughts after 2 years," I've been inspired by Joel's digital garden article. This space is inspired by a lot of other spaces, but adapted to my vision.

I really like the way Joel speaks about it: Joel Hooks, My blog is a digital garden, not a blog. This framing changed how I think about publishing online.

The phrase "digital garden" is a metaphor for thinking about writing and creating that focuses less on the resulting "content", and more on the process, care, and craft it takes to get there.

I think I've always struggled with the blog approach. I sometimes want to publish things that are not related to time. That can be true no matter when you read it. The opposite is true as well — some things are deeply temporal, like a TIL or a stream entry.

That's why the flux works alongside the garden: the garden holds evergreen pages; the flux captures the flow of time. Gwern makes a similar distinction with "tags" vs "essays." Some content is timeless reference, some is a snapshot of thinking at a moment.

§Technical Detail

The flux tool itself is simple Go. The core type is an Entry struct with a Kind discriminator. Each source implements a Fetch method:

// Source is the interface all flux providers implement.
type Source interface {
    Name() string
    Fetch(ctx context.Context, since time.Time) ([]Entry, error)
}

The aggregator merges entries, sorts by date, deduplicates by ID, and feeds the result to renderers (JSON Feed, Atom, HTML template). No framework. No JavaScript. Just static files deployed via rsync.

Why no framework?

As Kenneth Reitz puts it: "If Fly.io disappears tomorrow, I can have this running on a Raspberry Pi in 10 minutes. Portability is a form of independence." Same principle here. The output is pure HTML+CSS that any web server can host. The Go binary is the only moving part.