Flux — Design Sandbox
Exploring typography, rhythm, and stream design for vincent.demeester.fr
§§Typography Specimen
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
The quick brown fox jumps over the lazy dog. 0123456789
The quick brown fox jumps over the lazy dog. — semibold
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.
§§Link Styles
Links are styled following Adactio's guidance: subtle underline offset, thin decoration, translucent color. Hover reveals full underline. Compare:
-
Default styled link —
text-underline-offset: 0.15em -
Thicker underline —
text-decoration-thickness: 2px -
More offset —
text-underline-offset: 0.3em -
Accent-colored underline —
text-decoration-color: accent -
Dotted underline —
text-decoration-style: dotted
§§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;
};
}
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
A general note or annotation. Use for supplementary information the reader might find useful but isn’t essential to the main flow.
Use flux generate --dry-run to preview what entries would be added without writing any files. Handy when adding a new source.
The JSON Feed spec v1.1 allows an _extension namespace for custom fields. We use _flux to store entry kind and source metadata.
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.
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.
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
§Margin figure (Tufte-style)
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)
§Screenshot (drop shadow)
§Full-width figure
§Figure grid — 2 columns
§Figure grid — 3 columns
§No caption (bare image)
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).
- This is the current marker (✧ white star)
- A second item to see the rhythm
-
And a third with a link and
inline code
§Alternatives
- Reference mark (※)
- Japanese-influenced
- Unusual but elegant
- Hyphen bullet (⁃)
- Like a dash but shorter
- Understated
- En dash (–)
- Typographically clean
- Common in European typography
- Hedera (❧)
- Typographic fleuron
- Distinctive, bookish
- Ordered lists stay numbered
- Nothing fancy here
- 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.
Current accent: #4a7c59
§Light Mode
§Dark Mode
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.
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.
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.
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.
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'))
§§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.
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.