Skip to content

Repository files navigation

Entropic
========

Entropic is a retro textmode blog theme for purists.

The implementation uses Astro, Vite, and Tailwind CSS utilities. The surface
uses fixed-width text, ASCII frames, volume indexes, ANSI colors, bitmap CJK
glyphs, and plain `.phile` documents.


Configuration
-------------

Edit `entropic.config.ts` at the repository root. It is the single entry for
site identity, home sections, 88x31 buttons, decorative artwork, colors,
effects, volume metadata, CVE records, analytics, and optional WKD publishing.
You do not need to edit files inside `src/` to customize these settings.

The checked-in file is also the personal site configuration. `home.asciiArt`
inherits the banner from `src/config/defaults.ts` unless overridden; WKD and
the CVE page are explicitly enabled.
Set `cves.enabled: false` to omit the CVE page, its homepage links, and its sitemap
entry; the records can remain in `cves.records` for later use.
Article inspection and fragment links are enabled by default. Select article
text to inspect a value or prepare a link to that passage, then copy with your
browser's native Copy action. Fragment links locate the quoted text and briefly
highlight it; repeated passages need enough context to identify one match.
Set `philes.inspect: false` or `philes.fragmentLinks: false` to disable either
tool. Disabling fragment links also disables incoming passage navigation.

The file is a plain TypeScript object with `as const satisfies EntropicConfig`.
Your editor provides field descriptions and completion, including artwork
preset names and rotation modes. Omitted settings inherit internal defaults;
objects merge at documented leaves and arrays replace their defaults.

    pnpm check:config
        validate values, artwork selection, and optional public key input

    pnpm check
        also check unknown fields/types, source boundaries, and formatting

    pnpm dev
        preview configuration edits locally

Field comments in `entropic.config.ts` describe supported values, examples,
and default inheritance. The configuration types live in `src/config/types.ts`.


Directory Layout
----------------

    entropic.config.ts      the user-editable configuration
    src/config/             internal types, defaults, validation, and projections
    src/content/philes/     article source files
    src/pages/              Astro file routes and HTTP output adapters
    src/features/           home, site-badges, philes, volumes, cves, seo, and wkd
    src/shared/textmode/    text cells, ANSI, bitmap CJK, and browser behaviors
    src/shared/browser/     browser lifecycle helpers
    src/layouts/            shared page shells
    src/styles/             foundation, layer ordering, and responsive rules
    src/assets/decorations/  original artwork grouped by series
    src/generated/          generated glyph metadata
    scripts/                font generation, checks, and Git hooks
    tests/                  configuration, content, browser, and security regressions

Features own their data access, rendering, components, and styles. Shared
textmode primitives have separate pure, server, and browser entry points.
Cross-feature imports use public entries; `pnpm check:boundaries` verifies
these interfaces and keeps server dependencies out of browser modules.

Tailwind handles common layout and spacing. Pixel typography, ANSI/CJK output,
and animation rules live in native CSS. Theme utilities use the typed
configuration; Preflight is not enabled. Astro bundles styles from their owning
components and modules instead of loading every feature's CSS on every page.


Content
-------

Articles are `.phile` files under:

    src/content/philes/volume-*/**/*.phile

A phile is a frontmatter block followed by plain text.

Required fields:

    title
    date

Optional fields:

    order
    slug
    redacted
    lang
    decoration
    math

Credits come from each file's Git history, including renames and
Co-authored-by trailers.  The original author appears first, followed by
contributors in order of first participation.  GitHub nicknames are preferred;
accounts without a nickname use their login.  Unlinked identities keep their
Git author name.  A new, uncommitted article has no credits yet.

Names are resolved once per production build and embedded in the static pages.
No GitHub requests run in the browser.  Builds restore shallow history before
reading credits; Vercel checkouts without an origin use its Git system variables.
An optional build-time GITHUB_TOKEN increases the API request allowance.  Cached
profiles provide a fallback when GitHub is unavailable.

The body is rendered as textmode content.  It preserves layout, wraps to the
configured article width, renders images, accepts ANSI escape sequences, and
supports the custom ink block syntax.

Set `math: true` in an article's frontmatter to render LaTeX formulas with
Temml. Use `$...$` or `\(...\)` inline, and `$$...$$` or `\[...\]` for display
equations. Standalone tall expressions also receive display space. Math is
converted to native MathML at build time; unsupported
commands fail the build. Code spans and fenced code keep their TeX literal.
The bundled Entropic Math font derives from STIX Two Math's CFF font:
https://github.com/stipub/stixfonts/releases/latest
Its license is included in `fonts/STIXTwoMath.LICENSE`.
Radical size variants have their short horizontal caps trimmed so
MathML's overbar joins directly to the diagonal without a pixel step. Other
glyph outlines are unchanged. Fraction, radical, overbar and underbar rule
thicknesses use 0.05em to avoid two-pixel bars at the article's 16/17px sizes.
Run `pnpm fonts:math` to regenerate from the latest upstream release (requires
`uv`). The script checks the radical geometry before replacing the font and
records the resolved source URL and SHA-256 in the font's metadata. Normal
builds and page visits use the bundled font without fetching upstream.
Temml requires a browser with native MathML support. Equation references
(`\ref` and `\eqref`) are not wired into the site's rendering pipeline yet.

Math articles retain the textmode font and colors, with unframed equations
aligned to the body. Prose wraps to the viewport; long equations scroll within
their own region instead of shrinking the entire article. Articles without
`math: true` retain the original text rendering, including literal dollar signs.

Ink style reference:

    https://www.cubeyond.net/volume/3/ansi-ink-phile/

Article header artwork is chosen from the article's public URL. It stays the
same across reloads, rebuilds, and adding/removing other articles. Renaming its
slug or moving it to another volume can choose a different effect. The default
pool is `life`, `maze` (depth-first maze carving), `sort` (insertion sort),
`bits` (four byte registers), `asm` (a small assembly loop), and `reorder`
(out-of-order execution with in-order retirement). No extra controls interrupt
the reading layout.

Configure the pool and playback in `entropic.config.ts`:

    philes: {
      decoration: {
        effects: ["life", "maze", "sort", "bits", "asm", "reorder"],
        animated: true,
        speed: 1
      }
    }

Omitted options inherit these defaults. `effects` must contain at least one
unique algorithm; use a single item to show the same effect everywhere.
Reordering the pool does not reshuffle articles. For the new effects, `speed`
ranges from 0.25 to 4 and `animated: false` keeps a still frame. The new artwork
also stays visible without JavaScript. Life uses its original renderer, grid,
color, and timing; these playback options do not change it. All effects
pause offscreen, in hidden tabs, and for reduced motion.

Set `decoration: maze` (or any other effect name) in an article's frontmatter
to pin its effect, including one outside the global pool. `decoration: false`
removes that article's artwork and closes the unused header space.
`philes.decoration: false` disables all article artwork, including overrides.

The processor illustrations use native Gohu text at the article's font size.
`bits` highlights the bits changed by XOR, rotate, and shift operations. `asm`
shows an executed instruction with its resulting registers and flags. `reorder`
shows dependency waits and two execution slots; completed instructions remain
in the queue until all earlier instructions can retire.

Example:

    ---
    title: "Example"
    date: 2026-05-31
    ---

    --[ Summary ]-------------------------------------------------------------

        target    : ...
        primitive : ...

    --[ ink ]--
    | 00400000-00401000 r--p 00000000 ./a.out     ; ELF headers
    ~ BBBBBBBB.BBBBBBBB GGKK KKKKKKKK MMMMMMM
    | 00401000-00402000 r-xp 00001000 ./a.out     ; executable text
    ~ BBBBBBBB.BBBBBBBB GGRR KKKKKKKK MMMMMMM

    #[B|B] address range
    #[G|G] readable permission
    #[R|R] executable permission
    #[K|K] offset / padding
    #[M|M] mapped object


Volumes
-------

Volume numbers come from the directory name:

    volume-0
    volume-1
    volume-2

The public routes are:

    /volume/<n>/
    /volume/<n>/<phile-name>/

Per-volume behavior is configured in `entropic.config.ts` under `volumes`.
Only the fields supplied for a volume override its generated defaults.

Set `decoration` to `circuit`, `archive`, `study`, or `prism` for a themed header
illustration. `life` keeps the original Game of Life frame and is the default
for unconfigured volumes; `false` removes the decoration. For example:

    volumes: {
      0: { title: "Security Research", decoration: "circuit" },
      1: { title: "Historical Philes", decoration: "archive" }
    }

The four illustrations also accept an options object:

    decoration: {
      kind: "study",
      animated: true,
      speed: 1,
      calendar: { month: 12, day: 31 }
    }

`animated: false` keeps a static illustration. `speed` is a playback multiplier
from 0.25 to 4: 0.5 is half speed, 2 is double speed. These optional settings
default to `true` and `1`. `calendar` is only for `study` and defaults to December
31; use a numeric month and a valid day (February 29 is supported). The original
`life` frame uses the string shorthand. Every CPU transfer independently samples
an 8-bit value, including 00 and FF; consecutive random values may repeat.

The new illustrations remain visible without JavaScript. Their motion pauses
offscreen, in hidden tabs, and when reduced motion is requested. Article
headers use the separate `philes.decoration` settings described above.


Redactions
----------

A redacted phile uses:

    redacted: true

The loader discards redacted bodies from the content store and the public page
renders the redacted screen. The pre-commit hook parses staged YAML with the
same parser and unstages redacted philes, leaving the worktree untouched.
Install dependencies before using the hook. Invalid frontmatter blocks the
commit before the hook changes the index.

Redaction is a publishing flag, not secret storage: it does not erase files
from existing Git history. Keep confidential source material outside Git.

Enable hooks in a fresh clone:

    git config core.hooksPath scripts/hooks


Site Badges and Artwork
-----------------------

`entropic.config.ts` groups links and decoration under `buttons`. The default
rotates through all 25 presets daily in `Asia/Shanghai`:

    buttons: {
      items: [/* your 88x31 links or copy controls */],
      shuffleOnBuild: true,
      artwork: {
        mode: "daily",
        timeZone: "Asia/Shanghai",
        seed: "entropic-artwork-v1",
        fallback: "nagaraNozomi"
      }
    }

All visitors with the same date see the same composition, including after a
refresh. Each cycle shuffles every selected preset once and avoids repeats at
the cycle boundary. The date is checked on load and when returning to a tab or
cached page; a continuously open page does not swap on a timer.
No daily rebuild, endpoint, cookie, or browser storage is needed.

Use `artwork: false` for links alone, or
`artwork: { mode: "fixed", preset: "nagaraNozomi" }` for a fixed image.
An empty `items` array hides the panel. The inline configuration comments cover
custom images, preset pools, interval/visit modes, and the no-JavaScript fallback.

`shuffleOnBuild: true` shuffles buttons once per production build and keeps
that order across visits and artwork changes. Local development uses items order.
Each item supplies `label`, `imageSrc`, and either `href` or `copyText`.

Artwork originals live under `src/assets/decorations/<series>/`. Presets retain
the approved display sizes, overlap, and companion placement. Astro builds 1x
and 2x WebP images with lossless encoding; originals remain unchanged. Inert HTML
templates keep unselected compositions from fetching images. The shareable
animated site badge stays at `/assets/88x31/button.gif` for other sites to link to.

CSS grid and container queries own layout. Overlapped badges appear above the
artwork on hover or keyboard focus; touch devices show them immediately.
Artwork is decorative and cannot intercept link activation.


Commands
--------

Use Node.js 24 or newer and the pinned pnpm 12.3.4. `pnpm check:types` runs
Astro diagnostics with the official TS 6 compatibility API and the native
TypeScript 7 checker. Keep both aliases until Astro supports the new API.

    pnpm install --frozen-lockfile
        install dependencies

    pnpm dev
        run the local development server

    pnpm check
        run Astro and TypeScript diagnostics, formatting/lint, and module boundaries

    pnpm format
        format TS/JS/CSS/JSON with Biome and Astro templates with Prettier

    pnpm lint
        lint the files covered by Biome

    pnpm build
        build the static site

    pnpm test
        run content, textmode, redaction-hook, and WKD regression tests

    pnpm exec playwright install chromium firefox
        install the browser revisions used by this Playwright version

    pnpm test:browser http://127.0.0.1:4329
        check fitting, animation lifecycle, and site badges against a production preview
        set PLAYWRIGHT_BROWSER=firefox to use Firefox

    pnpm test:wkd
        verify optional WKD generation and key handling

    pnpm assets:fonts
        regenerate font subsets and CJK atlases

Font assets are committed.  Normal production builds do not require local font
tooling.

Run `pnpm assets:fonts` only when the committed font outputs need to change:

    after editing font source files under `fonts/`
    after changing CJK glyph coverage or bitmap atlas generation
    after changing text that introduces new CJK glyphs which must render via
        the bitmap CJK atlas
    after changing the font subset scripts themselves

The command regenerates:

    public/assets/fonts/gohu-subset.woff
    public/assets/cjk/wqy-cjk-body.png
    public/assets/cjk/wqy-cjk-link.png
    src/generated/cjk-atlas.ts

Regenerating these assets requires `fonttools`.  If `pyftsubset` is already on
`PATH`, the scripts use it directly.  Otherwise, with `uv` installed, the
scripts resolve `fonttools` automatically and keep the uv cache under `/tmp`.


Branches
--------

`master` is stable.  `dev` is integration.

Feature and fix work should branch from `dev`.  Pull requests should be
squash-merged.  Commits should be signed and should use Conventional Commits.


Public Key Discovery
--------------------

This personal site enables WKD for `root@cubeyond.net` in `entropic.config.ts`.
A new site that omits WKD settings leaves publication disabled. To configure it:

    wkd: {
      enabled: true,
      email: "hello@example.org",
      publicKeyPath: "public/assets/pgp/public-key.asc"
    }

Set `wkd.enabled: false` to disable publication while retaining the email and path. Replace the referenced file with your
public key export; its manual download URL also works without WKD. Key changes
require replacing only that file and rebuilding.

The configuration check rejects missing settings, private keys, and public
keys whose UIDs do not match the email. The development server watches the
configured public key file. Production requires a new deployment.

Migration: move the former `WKD_EMAIL` and `WKD_PUBLIC_KEY_PATH` values from
`.env.local` or the hosting environment into this object. Those old environment
variables no longer configure WKD; there is one source of truth. The email and
public-key path are public publishing settings, not credentials.

The build derives the mail domain and lookup hash, then generates the policy
and binary public key under `dist/.well-known/openpgpkey/`.  Generated WKD
files are not committed.  The Vercel header rules work with any mail domain.

For pnpm 12 on Vercel, use the pinned `packageManager` via Corepack
(`ENABLE_EXPERIMENTAL_COREPACK=1` in the build environment). Confirm the actual
Node.js and pnpm versions in the deployment log; Vercel's default package-manager
detection is not the same as honoring this pin. Local builds do not verify the
remote environment.

For Vercel, deploy the configured public key and attach
`openpgpkey.<mail-domain>` directly to that environment, without a domain
redirect.  Redeploy after changing the configuration. In Cloudflare DNS, add a
CNAME named `openpgpkey` using Vercel's displayed target, with Proxied (orange
cloud) and TTL Auto.  Use Full (strict) for the connection to Vercel and wait
for domain verification, HTTPS certificates, and DNS caches to update.

Preserve Vercel's proxy prerequisites for certificate issuance and renewal:
allow HTTP access to `/.well-known/acme-challenge/*` without redirects or
browser checks, and leave `/.well-known/vercel/*` uncached.  See:
https://vercel.com/docs/security/reverse-proxy

The proxy setting reflects deployment testing: direct Vercel connections
failed during TLS 1.3 shutdown with GnuPG 2.5.21 / GnuTLS 3.8.13, while the
same WKD lookup succeeded through Cloudflare with default TLS settings.
Other static hosts need equivalent WKD response headers and their own live
discovery checks.

WKD clients need public access without authentication or browser challenges.
Cloudflare's Browser Integrity Check can reject non-browser clients with
HTTP 403 / error 1010 even when GnuPG succeeds.  Add a scoped exception:

1. Select your domain in Cloudflare, then open Rules > Overview.
2. Select Create rule > Configuration Rule and name it `WKD public access`.
3. Under When incoming requests match, choose a custom filter expression
   and open Edit expression.  Replace the example hostname with your WKD
   hostname:

       (http.host eq "openpgpkey.example.org"
        and starts_with(http.request.uri.path, "/.well-known/openpgpkey/")
        and http.request.method in {"GET" "HEAD"})

4. Under Then the settings are, add Browser Integrity Check and set it to
   Off.  This exception applies only to WKD GET/HEAD requests on that host.
5. Select Deploy.

Cloudflare documents configuration rules and selective BIC exceptions here:
https://developers.cloudflare.com/rules/configuration-rules/create-dashboard/
https://developers.cloudflare.com/waf/tools/browser-integrity-check/#disable-selectively

After DNS changes have propagated, verify discovery with a fresh keyring:

    gpg --homedir "$(mktemp -d)" --auto-key-locate clear,wkd \
        --locate-keys hello@example.org

Use your configured email and compare the complete primary fingerprint with
your trusted public key export.  Check that policy and key URLs accept both
GET and HEAD with the expected content types and CORS headers.  Test generic
HTTP clients too: a successful GnuPG lookup alone does not rule out a
Cloudflare browser check blocking another client.  Local builds cannot
verify production DNS, HTTPS, or the hosting provider's access rules.

Sponsor this project

Used by

Contributors

Languages