From 9f6a2d0e9dd327805ad79d25c9cd2ba89fd54668 Mon Sep 17 00:00:00 2001 From: 37ng Date: Tue, 8 Sep 2026 09:20:50 -0700 Subject: [PATCH 1/2] refactor: one folder per post for content and assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything a post owns now lives under its own name: src/assets// for binaries and src/content/posts// for index.mdx plus that post's data files. Bitcoin's bars, epochs, off-chain revenue and the two new ms-by-month / ms-by-pool tables move out of src/lib/, which keeps only logic and site-wide modules. URLs are unchanged: the glob loader strips a trailing /index from the id, so posts/bitcoin/index.mdx still serves /bitcoin, and its `**/*.mdx` pattern means the JSON beside the prose is never read as an entry. npm run art now mirrors subfolders, so a master at art-src/bitcoin/bg.png writes src/assets/bitcoin/bg.webp and the one-folder-per-post rule holds for art too. Also fixes .prettierignore, which pointed at src/content/blog/*.mdx — a directory that no longer exists. Co-Authored-By: Claude Opus 5 --- .prettierignore | 2 +- CLAUDE.md | 14 +- package.json | 2 +- scripts/generate-art.mjs | 23 +- scripts/generate-bitcoin-epochs.mjs | 4 +- .../{bitcoin-bg.webp => bitcoin/bg.webp} | Bin src/assets/{bitcoin.glb => bitcoin/coin.glb} | Bin src/components/diagrams/BitcoinCoin3D.tsx | 4 +- .../posts/bitcoin/bars.json} | 0 .../posts/bitcoin/epochs.json} | 0 .../posts/{bitcoin.mdx => bitcoin/index.mdx} | 2 +- src/content/posts/bitcoin/ms-by-month.json | 494 ++++++++++++++++ src/content/posts/bitcoin/ms-by-pool.json | 534 ++++++++++++++++++ .../bitcoin}/offchain-revenue-jan2023.json | 0 .../{prologue.mdx => prologue/index.mdx} | 0 src/layouts/PostLayout.astro | 8 +- src/lib/bitcoin-timeline.ts | 2 +- src/lib/offchain-revenue.ts | 2 +- 18 files changed, 1063 insertions(+), 28 deletions(-) rename src/assets/{bitcoin-bg.webp => bitcoin/bg.webp} (100%) rename src/assets/{bitcoin.glb => bitcoin/coin.glb} (100%) rename src/{lib/bitcoin-bars.json => content/posts/bitcoin/bars.json} (100%) rename src/{lib/bitcoin-epochs.json => content/posts/bitcoin/epochs.json} (100%) rename src/content/posts/{bitcoin.mdx => bitcoin/index.mdx} (97%) create mode 100644 src/content/posts/bitcoin/ms-by-month.json create mode 100644 src/content/posts/bitcoin/ms-by-pool.json rename src/{lib => content/posts/bitcoin}/offchain-revenue-jan2023.json (100%) rename src/content/posts/{prologue.mdx => prologue/index.mdx} (100%) diff --git a/.prettierignore b/.prettierignore index 586d726..315e403 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,4 @@ node_modules package-lock.json # Bilingual blog prose — avoid Prettier reflowing/mangling content mixed with JSX. -src/content/blog/*.mdx +src/content/**/*.mdx diff --git a/CLAUDE.md b/CLAUDE.md index 9f3def7..f4b4eea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Prettier runs automatically on staged files via the `.githooks/pre-commit` hook. - **The stage** (`src/components/HeroStage.astro`): one full-screen subject at a time — backdrop art, live furniture (the bitcoin coin), and the bottom-left caption, all belonging to the same post and swapping together. Every post in the fan has a stage; the prologue gets `stages/PrologueSheet.tsx`, a sheet of manila carrying a pencil signature (the same component draws the prologue's card thumbnail in the fan, so the two can never drift), and any other post with no `stageImage` gets a plain surface in its `stageTone` — and `index.astro` derives `light` for it rather than reading `stageTone`, since paper is always light and trusting the field there fails invisibly (white type on cream) instead of visibly. **The homepage does not scroll** — every layer is fixed to the viewport and the fan is the only way through the work; `LANDING_STAGE` in `index.astro` is what a first visit sees. Which subject is up is ``. Hovering a card is the only thing that writes it (`setStage`, `src/lib/stage.ts`) and **nothing clears it**: a stage stays up after the fan drops, and is saved to `sessionStorage` so opening a post and coming home returns you to it rather than to the landing stage. **Visibility is pure CSS, and it has to be** — the browser paints `HeroStage`'s markup long before the island hydrates, so a stage chosen in JSX shows the landing stage first and cross-fades off it on the way home. So `HeroStage.astro` renders every stage and holds no state — it is **not an island**; only `` inside it runs on the client. The per-id rules and the `` bootstrap that sets `data-stage` before the body is parsed are both **generated from the post list in `index.astro`** (adding a post needs no edit). Adding new per-stage furniture means writing another block in `HeroStage.astro` by hand: a client directive is compile-time, so it can't be looked up from a map the way the old React version did. Two traps if you touch that bootstrap: the storage key must stay in step with `stage.ts`, and it must stay one `var`-only IIFE — a view transition re-executes it in the same window, so a top-level `const` (what `define:vars` emits) throws on the second run. It also re-applies on `astro:after-swap`, since a swap replaces ``'s attributes. - **Stage → post**: clicking the stage caption opens that blog post, which has richer interactive animations per concept and an optional link to the project's GitHub repo. -- **Index**: `src/components/PostsFan.tsx` (`client:load`) — a hand of cream index cards fanned along the bottom edge of the landing page. It is furniture, not an overlay: `--fan-peek` of each card stays on screen at rest, and the hand rises when the pointer reaches it (or on focus, or on tap). Every card is a link — hovering raises that post onto the stage (`setStage`), which is also what lifts the card out of the hand; the fan keeps no selection state of its own. While the hand is up, the card's title and description are written centered above it and the stage caption steps aside — same content, only the position changes, so lowering the hand is a handover rather than a reset. Both write the same `N-00x · date` index line, so the handover carries the number too. That number is the post's **position in the fan's order**, which is why `CURATED_ORDER`/`sortCurated`/`cardNo` live in `src/lib/fan.ts` and not in the island: `index.astro` sorts the one list before handing it to both, so the static caption and the hydrated panel can never number a post differently. Card **N-000** is the prologue: not a bespoke component, just `src/content/blog/prologue.mdx` pinned first in `CURATED_ORDER`, same as every other post. Live per-stage furniture (the bitcoin coin) lives in `STAGE_FURNITURE` in `HeroStage.tsx`, mounted once into a fixed layer rather than inside a snap section: a card can raise it from any scroll position, and one WebGL context is enough. +- **Index**: `src/components/PostsFan.tsx` (`client:load`) — a hand of cream index cards fanned along the bottom edge of the landing page. It is furniture, not an overlay: `--fan-peek` of each card stays on screen at rest, and the hand rises when the pointer reaches it (or on focus, or on tap). Every card is a link — hovering raises that post onto the stage (`setStage`), which is also what lifts the card out of the hand; the fan keeps no selection state of its own. While the hand is up, the card's title and description are written centered above it and the stage caption steps aside — same content, only the position changes, so lowering the hand is a handover rather than a reset. Both write the same `N-00x · date` index line, so the handover carries the number too. That number is the post's **position in the fan's order**, which is why `CURATED_ORDER`/`sortCurated`/`cardNo` live in `src/lib/fan.ts` and not in the island: `index.astro` sorts the one list before handing it to both, so the static caption and the hydrated panel can never number a post differently. Card **N-000** is the prologue: not a bespoke component, just `src/content/posts/prologue/index.mdx` pinned first in `CURATED_ORDER`, same as every other post. Live per-stage furniture (the bitcoin coin) lives in `STAGE_FURNITURE` in `HeroStage.tsx`, mounted once into a fixed layer rather than inside a snap section: a card can raise it from any scroll position, and one WebGL context is enough. - **Header**: site label on the left, live clock on the right. No nav links. ## Blog @@ -25,7 +25,7 @@ Each blog is an MDX file that combines article content with interactive illustra **Routing:** File-based. `src/pages/index.astro` is the homepage. `src/pages/blog/[slug].astro` is the dynamic blog route — it calls `getStaticPaths()` to pre-render one page per MDX post at build time. -**Content:** Blog posts live in `src/content/blog/*.mdx`. The schema (title, description, date, tags) is enforced in `src/content.config.ts` via Zod. Posts can import and render any React component inline. +**Content:** One folder per post under `src/content/posts//`, holding `index.mdx` plus that post's own data files (`bitcoin/bars.json`, `bitcoin/ms-by-pool.json`, …). The collection loader globs `**/*.mdx`, so the JSON sitting beside the prose is never mistaken for an entry, and `index` is stripped from the id — `posts/bitcoin/index.mdx` still serves `/bitcoin`. Post-scoped data is imported from there (`@/content/posts/bitcoin/bars.json`) rather than from `src/lib/`, which now holds only the logic and the site-wide modules. The schema (title, description, date, tags) is enforced in `src/content.config.ts` via Zod. Posts can import and render any React component inline. **Per-post color — two axes, deliberately separate.** `color` in frontmatter is **identity**: one custom hue per post, free to be loud, never required to be legible over anything. It drives `--hero-accent` (the fan's `N-00x · date` line, the stage caption's hover) and the post page's own `--accent`. `stageTone: "dark" | "light"` is **legibility**: it declares whether that post's hero art is dark or light, and selects a whole set of readable tones — `--stage-title` and `--stage-body` for the caption, and the same `--stage-title` for the header chrome (site label + clock), which sits directly on the art. Both are emitted as per-id rules generated in `index.astro`; the light pair is named in `global.css` (`--stage-title-light`/`--stage-body-light`) because Tailwind drops any `@theme` token that nothing in that stylesheet references, and the generated CSS is invisible to it. A post that sets neither gets signal orange and the dark tones. @@ -35,19 +35,19 @@ Each blog is an MDX file that combines article content with interactive illustra **Design language:** "Engineering plan set / spec sheet." Cold blue-slate `ink` neutrals + one oxide-orange `signal` accent (`#c2410c`-family) — signal appears only on rules, indices, and hover states, never on body text. Mono (`font-mono`, JetBrains Mono) carries all UI chrome — nav, labels, dates — uppercase with wide tracking (enforced globally by a `.font-mono` rule in `global.css`, not per-component). Fraunces drives its variable axes (`opsz`/`SOFT`/`WONK`) on every heading, sitewide, not just in prose. Zero radius everywhere (`--radius-*: 0`); panels use hairline rules (`shadow-panel` token), not blurred drop shadows. Interactions are mechanical — no opacity fades or translate-Y lifts; see `.spec-card` (hard offset border, print-misregistration style) and `.nav-index` (mono index number that shutters open, not fades in) in `global.css`. Apply `prose-tech` class to article content for the blog prose styles — includes auto-numbered `§01`/`§02` section counters on `h2`. Every `h2`/`h3` also gets a hover-revealed `#` permalink (`.heading-anchor`, injected at build time by `rehype-autolink-headings` in `astro.config.mjs`). -**Onchain revenue timeline** (`src/components/diagrams/BitcoinTimeline.tsx`, on the bitcoin stage and in `bitcoin.mdx`): one bar per period, and every figure is a **period total** — what the chain earned over the whole bar, roughly a month — never an average per block, since dividing by however many blocks were mined is the one part of the question nobody asked. The bar is **tx fees only**, priced in **dollars of the day**: a fee in BTC says nothing across seventeen years, because the unit itself changed value ten-thousandfold along the axis it is plotted against, so each row carries its own BTC/USD and a 2012 month is priced at 2012 rates. The x axis is **block height, not the calendar**: one bar per **4,375 blocks**, which is 210,000 / 48, so a halving is always a bar _edge_ and never a point inside a bar (a calendar month would drift across boundaries). Subsidy is never stored — `subsidyAt(height)` is `50 / 2 ** floor(h / 210_000)`, and `subsidyBtcOverBar` scales it to the bar so it can be added to a period total — and neither is the dollar figure, which is one multiplication away from the price each row carries (`usdWorth`). The log scale is floored at a dollar (`MIN_USD`) — a 2009 month of fees is worth hundred-thousandths of a cent, and letting that end in stretches the scale over thirteen orders and pins everything after 2012 to the top. `src/lib/bitcoin-bars.json` is **pseudo data**: the heights are exact and the month labels follow the real halving dates, but the fees and the price are invented. There is **no generator script** for it — replace the file with real per-bar measurements. Reading is by hover only: the bar under the pointer is redrawn in the accent — one mark, the same colour as the halving tick and the axis cursor — and the cursor stays where the pointer left rather than snapping back to the last bar. It also carries the **annualized percentage**: a year of that bar's pay over every coin issued (`onchainShare`, annualized by `BARS_PER_YEAR` since the rows are period totals), with `supplyAt` walking the halving schedule the way `subsidyAt` does and `supplyAfter` supplying an end-of-bar denominator that is never zero. Both sides are BTC, so the price cancels and that column owes nothing to the price beside it. Nothing animates, and nothing is fetched at runtime (an earlier version grew the bars in on a `requestAnimationFrame`, which never fires in a hidden tab and left the chart at `scaleY(0)`). +**Onchain revenue timeline** (`src/components/diagrams/BitcoinTimeline.tsx`, on the bitcoin stage and in `bitcoin.mdx`): one bar per period, and every figure is a **period total** — what the chain earned over the whole bar, roughly a month — never an average per block, since dividing by however many blocks were mined is the one part of the question nobody asked. The bar is **tx fees only**, priced in **dollars of the day**: a fee in BTC says nothing across seventeen years, because the unit itself changed value ten-thousandfold along the axis it is plotted against, so each row carries its own BTC/USD and a 2012 month is priced at 2012 rates. The x axis is **block height, not the calendar**: one bar per **4,375 blocks**, which is 210,000 / 48, so a halving is always a bar _edge_ and never a point inside a bar (a calendar month would drift across boundaries). Subsidy is never stored — `subsidyAt(height)` is `50 / 2 ** floor(h / 210_000)`, and `subsidyBtcOverBar` scales it to the bar so it can be added to a period total — and neither is the dollar figure, which is one multiplication away from the price each row carries (`usdWorth`). The log scale is floored at a dollar (`MIN_USD`) — a 2009 month of fees is worth hundred-thousandths of a cent, and letting that end in stretches the scale over thirteen orders and pins everything after 2012 to the top. `src/content/posts/bitcoin/bars.json` is **pseudo data**: the heights are exact and the month labels follow the real halving dates, but the fees and the price are invented. There is **no generator script** for it — replace the file with real per-bar measurements. Reading is by hover only: the bar under the pointer is redrawn in the accent — one mark, the same colour as the halving tick and the axis cursor — and the cursor stays where the pointer left rather than snapping back to the last bar. It also carries the **annualized percentage**: a year of that bar's pay over every coin issued (`onchainShare`, annualized by `BARS_PER_YEAR` since the rows are period totals), with `supplyAt` walking the halving schedule the way `subsidyAt` does and `supplyAfter` supplying an end-of-bar denominator that is never zero. Both sides are BTC, so the price cancels and that column owes nothing to the price beside it. Nothing animates, and nothing is fetched at runtime (an earlier version grew the bars in on a `requestAnimationFrame`, which never fires in a hidden tab and left the chart at `scaleY(0)`). -**Implied off-chain revenue figure** (`src/components/diagrams/OffchainRevenue.tsx`, in `bitcoin.mdx`): one mark per block that mined a transaction paying under its own floor feerate, `(floor_B − actual) × vsize` on a log scale, with a per-day coverage rail underneath for the blocks that carried nothing. The numbers in `src/lib/offchain-revenue-jan2023.json` are **pseudo data** — a deterministic stand-in written by `npm run offchain-pseudo`, there so the figure could be built before the real measurements exist. Real data replaces that one file and nothing else; the generator script then goes away. Two things the generator has to keep true, because the tooltip prints all three numbers side by side: `impliedSats` is derived from the _rounded_ feerates, so a reader who multiplies them out lands on the printed figure; and only flagged blocks are listed, with `totalBlocks`/`blocksPerDay` carrying the rest (the rail's denominator). Marks are painted through `.chart-mark` in `global.css` rather than a token utility — a post's `color` is chosen for identity, not contrast, so on the light theme's paper the figure swaps to `--accent-dark`. +**Implied off-chain revenue figure** (`src/components/diagrams/OffchainRevenue.tsx`, in `bitcoin.mdx`): one mark per block that mined a transaction paying under its own floor feerate, `(floor_B − actual) × vsize` on a log scale, with a per-day coverage rail underneath for the blocks that carried nothing. The numbers in `src/content/posts/bitcoin/offchain-revenue-jan2023.json` are **pseudo data** — a deterministic stand-in written by `npm run offchain-pseudo`, there so the figure could be built before the real measurements exist. Real data replaces that one file and nothing else; the generator script then goes away. Two things the generator has to keep true, because the tooltip prints all three numbers side by side: `impliedSats` is derived from the _rounded_ feerates, so a reader who multiplies them out lands on the printed figure; and only flagged blocks are listed, with `totalBlocks`/`blocksPerDay` carrying the rest (the rail's denominator). Marks are painted through `.chart-mark` in `global.css` rather than a token utility — a post's `color` is chosen for identity, not contrast, so on the light theme's paper the figure swaps to `--accent-dark`. -**Interactive diagrams:** `src/components/diagrams/` holds heavier React components (e.g. `BitcoinCoin3D.tsx`, `three` + `@react-three/fiber`). They must be hydrated explicitly at the call site — `client:load` in MDX posts, `client:idle` for the stage furniture in `HeroStage.astro`. Their binary payloads live in `src/assets/` and are pulled in with a `?url` import (`import modelUrl from "@/assets/bitcoin.glb?url"`), not from `public/`: the build then fingerprints the file for long-term caching and fails at compile time if it is ever moved or renamed, where a `public/` path only 404s at runtime. +**Interactive diagrams:** `src/components/diagrams/` holds heavier React components (e.g. `BitcoinCoin3D.tsx`, `three` + `@react-three/fiber`). They must be hydrated explicitly at the call site — `client:load` in MDX posts, `client:idle` for the stage furniture in `HeroStage.astro`. Their binary payloads live in that post's own `src/assets//` folder and are pulled in with a `?url` import (`import modelUrl from "@/assets/bitcoin/coin.glb?url"`), not from `public/`: the build then fingerprints the file for long-term caching and fails at compile time if it is ever moved or renamed, where a `public/` path only 404s at runtime. **Path alias:** `@/` maps to `src/` (used in MDX imports like `import { FlowDiagram } from '@/components/diagrams/FlowDiagram'`). ## Adding content -**Images.** Masters live in `art-src/`, which is **gitignored** — a multi-megabyte master committed once is in git history forever, and a second version doubles it. `npm run art` (`scripts/generate-art.mjs`) writes one stage-width webp per master into `src/assets/`, and _that_ is what gets committed (7 MB PNG → 696 KB webp). From there the build behaves normally: `index.astro` derives the fan's 480px card thumbnail with `getImage()`, while the stage uses the committed file as-is — re-encoding a webp that is already the right size and format buys nothing. `--quality` (default 90) and `--sizes` (default 2560) are flags on the script. +**Images.** Masters live in `art-src/`, which is **gitignored** — a multi-megabyte master committed once is in git history forever, and a second version doubles it. `npm run art` (`scripts/generate-art.mjs`) writes one stage-width webp per master into `src/assets/`, and _that_ is what gets committed (7 MB PNG → 696 KB webp). It mirrors subfolders, so the master for a post's stage art belongs at `art-src//.png` and lands at `src/assets//.webp`, one folder per post. From there the build behaves normally: `index.astro` derives the fan's 480px card thumbnail with `getImage()`, while the stage uses the committed file as-is — re-encoding a webp that is already the right size and format buys nothing. `--quality` (default 90) and `--sizes` (default 2560) are flags on the script. -**New blog post:** Create `src/content/blog/my-post.mdx` with frontmatter: +**New blog post:** Create `src/content/posts/my-post/index.mdx` with frontmatter, and keep that post's data files and stage art in the same-named folders (`src/content/posts/my-post/`, `src/assets/my-post/`): ```mdx --- diff --git a/package.json b/package.json index 6a1e19e..7500d40 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "verify": "npm run format:check && npm run check && npm run build && npm test", "art": "node scripts/generate-art.mjs", "bitcoin-epochs": "node scripts/generate-bitcoin-epochs.mjs", - "offchain-pseudo": "node scripts/generate-offchain-pseudo.mjs src/lib/offchain-revenue-jan2023.json", + "offchain-pseudo": "node scripts/generate-offchain-pseudo.mjs src/content/posts/bitcoin/offchain-revenue-jan2023.json", "test": "tsx --test src/**/*.test.ts" }, "dependencies": { diff --git a/scripts/generate-art.mjs b/scripts/generate-art.mjs index 603ed04..06c1c0b 100644 --- a/scripts/generate-art.mjs +++ b/scripts/generate-art.mjs @@ -1,6 +1,8 @@ /** * Turns a full-size master in `art-src/` into the one committed webp the site - * builds from, in `src/assets/`. + * builds from, in `src/assets/`. Subdirectories are mirrored, so a master at + * `art-src/bitcoin/bg.png` becomes `src/assets/bitcoin/bg.webp` — one folder + * per post, matching `src/content/posts/`. * * Why this exists rather than handing the master to Astro: a master is tens of * megabytes of lossless pixels, and committing one puts it in git history @@ -18,7 +20,7 @@ * npm run art # every master, default quality * npm run art -- --quality 100 # lossless-ish, bigger file * npm run art -- --sizes 1920 # a different stage width - * npm run art -- bitcoin # just one master, by name + * npm run art -- bitcoin/bg # just one master, by path */ import { mkdir, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -64,11 +66,14 @@ const kb = (bytes) => `${Math.round(bytes / 1024)} KB`; const { names, quality, sizes } = parseArgs(process.argv.slice(2)); -const entries = (await readdir(SRC_DIR)).filter( - (file) => - MASTERS.test(file) && - (names.length === 0 || names.includes(path.parse(file).name)), -); +const entries = (await readdir(SRC_DIR, { recursive: true })) + .map((file) => file.split(path.sep).join("/")) + .filter((file) => { + if (!MASTERS.test(file)) return false; + if (names.length === 0) return true; + const stem = file.replace(MASTERS, ""); + return names.some((name) => name === stem || name === path.basename(stem)); + }); if (entries.length === 0) { console.error( @@ -79,12 +84,12 @@ if (entries.length === 0) { process.exit(1); } -await mkdir(OUT_DIR, { recursive: true }); console.log(`webp q${quality} · widths ${sizes.join(", ")}`); for (const file of entries) { - const name = path.parse(file).name; + const name = file.replace(MASTERS, ""); const source = path.join(SRC_DIR, file); + await mkdir(path.join(OUT_DIR, path.dirname(name)), { recursive: true }); const master = await stat(source); console.log(`\n${name} (master ${kb(master.size)})`); diff --git a/scripts/generate-bitcoin-epochs.mjs b/scripts/generate-bitcoin-epochs.mjs index 5ae3aec..4534fef 100644 --- a/scripts/generate-bitcoin-epochs.mjs +++ b/scripts/generate-bitcoin-epochs.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Updates src/lib/bitcoin-epochs.json — one row per *finished* halving epoch, + * Updates src/content/posts/bitcoin/epochs.json — one row per *finished* halving epoch, * aggregated from public history, plus the average BTC/USD needed to say what * that epoch's own tx fees were worth at the time. * @@ -45,7 +45,7 @@ import { access, readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const OUT = fileURLToPath( - new URL("../src/lib/bitcoin-epochs.json", import.meta.url), + new URL("../src/content/posts/bitcoin/epochs.json", import.meta.url), ); // The protocol's own constants — a halving every 210,000 blocks, subsidy // halving each time from 50 BTC. Deriving epoch heights and subsidies from diff --git a/src/assets/bitcoin-bg.webp b/src/assets/bitcoin/bg.webp similarity index 100% rename from src/assets/bitcoin-bg.webp rename to src/assets/bitcoin/bg.webp diff --git a/src/assets/bitcoin.glb b/src/assets/bitcoin/coin.glb similarity index 100% rename from src/assets/bitcoin.glb rename to src/assets/bitcoin/coin.glb diff --git a/src/components/diagrams/BitcoinCoin3D.tsx b/src/components/diagrams/BitcoinCoin3D.tsx index ad36348..e76152d 100644 --- a/src/components/diagrams/BitcoinCoin3D.tsx +++ b/src/components/diagrams/BitcoinCoin3D.tsx @@ -10,10 +10,10 @@ import { } from "@react-three/drei"; // Imported as a URL rather than served from public/ so the build fingerprints // it and fails loudly if the file ever moves. -import modelUrl from "@/assets/bitcoin.glb?url"; +import modelUrl from "@/assets/bitcoin/coin.glb?url"; /** - * A downloaded bitcoin coin model (src/assets/bitcoin.glb — a Sketchfab export + * A downloaded bitcoin coin model (src/assets/bitcoin/coin.glb — a Sketchfab export * with baked baseColor/metallicRoughness/normal maps already built in, so no * custom material work is needed) rendered with a small studio lighting rig * for a polished-metal look. Drag to orbit the camera via OrbitControls; diff --git a/src/lib/bitcoin-bars.json b/src/content/posts/bitcoin/bars.json similarity index 100% rename from src/lib/bitcoin-bars.json rename to src/content/posts/bitcoin/bars.json diff --git a/src/lib/bitcoin-epochs.json b/src/content/posts/bitcoin/epochs.json similarity index 100% rename from src/lib/bitcoin-epochs.json rename to src/content/posts/bitcoin/epochs.json diff --git a/src/content/posts/bitcoin.mdx b/src/content/posts/bitcoin/index.mdx similarity index 97% rename from src/content/posts/bitcoin.mdx rename to src/content/posts/bitcoin/index.mdx index f291dc3..4878875 100644 --- a/src/content/posts/bitcoin.mdx +++ b/src/content/posts/bitcoin/index.mdx @@ -4,7 +4,7 @@ description: "Pools, farms and miners" date: "2026-07-28" feat: true tags: ["bitcoin", "crypto", "cryptocurrency"] -stageImage: "../../assets/bitcoin-bg.webp" +stageImage: "../../../assets/bitcoin/bg.webp" color: "#F7941D" stageTone: "dark" --- diff --git a/src/content/posts/bitcoin/ms-by-month.json b/src/content/posts/bitcoin/ms-by-month.json new file mode 100644 index 0000000..09e3f01 --- /dev/null +++ b/src/content/posts/bitcoin/ms-by-month.json @@ -0,0 +1,494 @@ +{ + "2023-12": { + "pools": { + "Foundry USA": 20870000 + }, + "sum": 20870000 + }, + "2024-01": { + "pools": { + "Foundry USA": 289390500 + }, + "sum": 289390500 + }, + "2024-02": { + "pools": { + "Foundry USA": 107362500 + }, + "sum": 107362500 + }, + "2024-03": { + "pools": { + "Foundry USA": 427250 + }, + "sum": 427250 + }, + "2024-04": { + "pools": { + "Foundry USA": 35782000 + }, + "sum": 35782000 + }, + "2024-05": { + "pools": { + "Foundry USA": 1091500, + "SpiderPool": 76000 + }, + "sum": 1167500 + }, + "2024-06": { + "pools": { + "Foundry USA": 1383775, + "SpiderPool": 288050, + "SBI Crypto": 1875 + }, + "sum": 1673700 + }, + "2024-07": { + "pools": { + "Foundry USA": 6165950, + "SpiderPool": 629675, + "MARA Pool": 404250, + "SBI Crypto": 215625, + "OCEAN": 5500 + }, + "sum": 7421000 + }, + "2024-08": { + "pools": { + "Foundry USA": 14494500, + "MARA Pool": 1005750, + "SpiderPool": 485750, + "SBI Crypto": 431500, + "F2Pool": 131000, + "AntPool": 9500, + "OCEAN": 3000 + }, + "sum": 16561000 + }, + "2024-09": { + "pools": { + "AntPool": 2919250, + "MARA Pool": 1670500, + "Foundry USA": 865500, + "SBI Crypto": 812500, + "F2Pool": 731750, + "SpiderPool": 621250, + "Braiins Pool": 485750, + "Binance Pool": 331250, + "Luxor": 37750, + "BTC.com": 23750, + "OCEAN": 19250, + "Poolin": 6000, + "ULTIMUSPOOL": 4000, + "WhitePool": 3000 + }, + "sum": 8531500 + }, + "2024-10": { + "pools": { + "Foundry USA": 16328301, + "AntPool": 14715680, + "F2Pool": 5453604, + "MARA Pool": 3902345, + "SpiderPool": 3167230, + "Binance Pool": 2388333, + "Braiins Pool": 1669612, + "Luxor": 1440809, + "Poolin": 1351936, + "ULTIMUSPOOL": 421640, + "BTC.com": 392397, + "SBI Crypto": 274805, + "WhitePool": 115499, + "OCEAN": 12750 + }, + "sum": 51634941 + }, + "2024-11": { + "pools": { + "AntPool": 8014382, + "Foundry USA": 7020606, + "F2Pool": 4686959, + "Binance Pool": 1086599, + "SpiderPool": 1018232, + "Luxor": 920806, + "Braiins Pool": 668212, + "MARA Pool": 310682, + "SBI Crypto": 228672, + "BTC.com": 148066, + "ULTIMUSPOOL": 147046, + "WhitePool": 97947, + "Poolin": 30084, + "OCEAN": 6272 + }, + "sum": 24384565 + }, + "2024-12": { + "pools": { + "AntPool": 5590764, + "Foundry USA": 4387602, + "F2Pool": 3542128, + "SpiderPool": 417230, + "Braiins Pool": 366154, + "SBI Crypto": 322918, + "BTC.com": 235914, + "Binance Pool": 227218, + "MARA Pool": 190139, + "Luxor": 164280, + "WhitePool": 141354, + "Poolin": 96390, + "ULTIMUSPOOL": 19169 + }, + "sum": 15701260 + }, + "2025-01": { + "pools": { + "Foundry USA": 2266197, + "AntPool": 2182291, + "F2Pool": 1449809, + "SpiderPool": 1019942, + "Luxor": 364773, + "MARA Pool": 110994, + "Binance Pool": 106917, + "Braiins Pool": 104725, + "WhitePool": 90102, + "BTC.com": 41685, + "SBI Crypto": 41377, + "ULTIMUSPOOL": 31515, + "Poolin": 21563, + "OCEAN": 2006 + }, + "sum": 7833896 + }, + "2025-02": { + "pools": { + "AntPool": 2210548, + "Foundry USA": 2093172, + "F2Pool": 1435272, + "Braiins Pool": 421127, + "SpiderPool": 256753, + "Binance Pool": 228560, + "Luxor": 208174, + "OCEAN": 200032, + "MARA Pool": 132128, + "BTC.com": 70486, + "SBI Crypto": 64587, + "WhitePool": 61639, + "Poolin": 59256, + "ULTIMUSPOOL": 16485 + }, + "sum": 7458219 + }, + "2025-03": { + "pools": { + "AntPool": 3763731, + "Foundry USA": 2854105, + "F2Pool": 1550967, + "MARA Pool": 686552, + "Luxor": 310360, + "Binance Pool": 295213, + "SpiderPool": 236223, + "SBI Crypto": 127030, + "Poolin": 101268, + "Braiins Pool": 66356, + "OCEAN": 41059, + "BTC.com": 30380, + "WhitePool": 30193 + }, + "sum": 10093437 + }, + "2025-04": { + "pools": { + "Foundry USA": 2264284, + "AntPool": 1856831, + "F2Pool": 1018941, + "SBI Crypto": 314408, + "Luxor": 221303, + "MARA Pool": 219197, + "SpiderPool": 217285, + "Binance Pool": 176287, + "Braiins Pool": 96823, + "BTC.com": 95364, + "WhitePool": 55508, + "Poolin": 41337, + "OCEAN": 17156, + "ULTIMUSPOOL": 15498 + }, + "sum": 6610222 + }, + "2025-05": { + "pools": { + "AntPool": 1685029, + "Foundry USA": 1536677, + "MARA Pool": 1034262, + "F2Pool": 705640, + "Luxor": 670949, + "SpiderPool": 524090, + "SBI Crypto": 368399, + "BTC.com": 123958, + "Binance Pool": 110943, + "Braiins Pool": 77196, + "Poolin": 70492, + "ULTIMUSPOOL": 30146, + "WhitePool": 14000, + "OCEAN": 13997 + }, + "sum": 6965778 + }, + "2025-06": { + "pools": { + "AntPool": 1811313, + "Foundry USA": 1180135, + "F2Pool": 745445, + "SpiderPool": 356462, + "MARA Pool": 178863, + "Binance Pool": 170246, + "Luxor": 166496, + "SBI Crypto": 150946, + "Braiins Pool": 64253, + "WhitePool": 63514, + "BTC.com": 39502, + "ULTIMUSPOOL": 28943, + "OCEAN": 2000 + }, + "sum": 4958118 + }, + "2025-07": { + "pools": { + "SpiderPool": 3914637, + "Foundry USA": 2712433, + "AntPool": 2369221, + "MARA Pool": 867399, + "F2Pool": 596915, + "Luxor": 106733, + "Binance Pool": 102766, + "WhitePool": 95495, + "Braiins Pool": 95365, + "SBI Crypto": 95028, + "BTC.com": 24081, + "ULTIMUSPOOL": 18500, + "Poolin": 11023 + }, + "sum": 11009596 + }, + "2025-08": { + "pools": { + "AntPool": 836454, + "SpiderPool": 638648, + "Foundry USA": 459454, + "Luxor": 183169, + "F2Pool": 164001, + "Braiins Pool": 123470, + "MARA Pool": 98430, + "Binance Pool": 59482, + "SBI Crypto": 59000, + "BTC.com": 38007, + "WhitePool": 21995, + "ULTIMUSPOOL": 18489, + "Poolin": 2000 + }, + "sum": 2702599 + }, + "2025-09": { + "pools": { + "AntPool": 605433, + "Foundry USA": 538116, + "SpiderPool": 436135, + "F2Pool": 328984, + "SBI Crypto": 192654, + "Braiins Pool": 161971, + "Luxor": 97496, + "Binance Pool": 78519, + "WhitePool": 59956, + "MARA Pool": 55005, + "ULTIMUSPOOL": 25000, + "BTC.com": 7000 + }, + "sum": 2586269 + }, + "2025-10": { + "pools": { + "AntPool": 590000, + "Foundry USA": 528157, + "F2Pool": 515639, + "SpiderPool": 145915, + "Luxor": 117233, + "Braiins Pool": 54017, + "SBI Crypto": 45498, + "ULTIMUSPOOL": 33999, + "MARA Pool": 24742, + "Binance Pool": 13984, + "WhitePool": 9995, + "BTC.com": 5990 + }, + "sum": 2085169 + }, + "2025-11": { + "pools": { + "Foundry USA": 383991, + "AntPool": 374100, + "F2Pool": 370429, + "SpiderPool": 220850, + "Luxor": 147446, + "MARA Pool": 145979, + "BTC.com": 74498, + "Binance Pool": 58992, + "Braiins Pool": 34944, + "SBI Crypto": 30595, + "WhitePool": 30519, + "Poolin": 15000 + }, + "sum": 1887343 + }, + "2025-12": { + "pools": { + "AntPool": 638977, + "Foundry USA": 500000, + "F2Pool": 421794, + "SpiderPool": 294851, + "Luxor": 165922, + "MARA Pool": 107518, + "WhitePool": 58474, + "Binance Pool": 50123, + "SBI Crypto": 26502, + "BTC.com": 19500, + "Braiins Pool": 14455, + "Poolin": 10500 + }, + "sum": 2308616 + }, + "2026-01": { + "pools": { + "Foundry USA": 674472, + "AntPool": 580562, + "F2Pool": 282964, + "SpiderPool": 118300, + "Luxor": 84570, + "MARA Pool": 76542, + "Binance Pool": 58866, + "Braiins Pool": 49505, + "SBI Crypto": 20487, + "ULTIMUSPOOL": 17001, + "Poolin": 2002, + "WhitePool": 2000 + }, + "sum": 1967271 + }, + "2026-02": { + "pools": { + "AntPool": 1888987, + "Foundry USA": 1790928, + "F2Pool": 492044, + "SpiderPool": 208964, + "Braiins Pool": 118017, + "Binance Pool": 96688, + "MARA Pool": 80779, + "Luxor": 72864, + "SBI Crypto": 22737, + "WhitePool": 22480, + "BTC.com": 13961, + "Poolin": 1000 + }, + "sum": 4809449 + }, + "2026-03": { + "pools": { + "Foundry USA": 984285, + "AntPool": 493644, + "SpiderPool": 402817, + "F2Pool": 301328, + "SBI Crypto": 99568, + "Luxor": 69545, + "MARA Pool": 44244, + "Binance Pool": 18008, + "ULTIMUSPOOL": 12000, + "Braiins Pool": 10618, + "BTC.com": 2000 + }, + "sum": 2438057 + }, + "2026-04": { + "pools": { + "Foundry USA": 1089833, + "AntPool": 413429, + "F2Pool": 327986, + "SpiderPool": 216576, + "Luxor": 156462, + "MARA Pool": 73020, + "Binance Pool": 68237, + "BTC.com": 46997, + "SBI Crypto": 39693, + "Braiins Pool": 9995, + "WhitePool": 5003, + "ULTIMUSPOOL": 4028, + "Poolin": 2000 + }, + "sum": 2453259 + }, + "2026-05": { + "pools": { + "Foundry USA": 1526329, + "AntPool": 1062561, + "SpiderPool": 307604, + "F2Pool": 259262, + "Luxor": 102890, + "SBI Crypto": 95051, + "Braiins Pool": 47041, + "MARA Pool": 42996, + "Binance Pool": 21926, + "ULTIMUSPOOL": 11015, + "WhitePool": 2001 + }, + "sum": 3478676 + }, + "2026-06": { + "pools": { + "Foundry USA": 73239643, + "AntPool": 55095441, + "F2Pool": 33702688, + "SpiderPool": 23890000, + "Luxor": 9638990, + "Braiins Pool": 7609621, + "MARA Pool": 7366645, + "SBI Crypto": 5616311, + "Binance Pool": 4460181, + "Poolin": 1761149, + "BTC.com": 1377470, + "ULTIMUSPOOL": 471820, + "WhitePool": 388869 + }, + "sum": 224618828 + }, + "2026-07": { + "pools": { + "AntPool": 1422032, + "Foundry USA": 1247857, + "SpiderPool": 1000773, + "F2Pool": 906318, + "Luxor": 416136, + "MARA Pool": 108506, + "BTC.com": 104073, + "Binance Pool": 86443, + "SBI Crypto": 76128, + "WhitePool": 21462, + "Braiins Pool": 19471, + "Poolin": 7996 + }, + "sum": 5417195 + }, + "2026-08": { + "pools": { + "Foundry USA": 1165252, + "AntPool": 1091355, + "F2Pool": 635507, + "SpiderPool": 158506, + "Luxor": 147752, + "Binance Pool": 114082, + "BTC.com": 88528, + "MARA Pool": 42943, + "Braiins Pool": 36014, + "WhitePool": 7526 + }, + "sum": 3487465 + } +} diff --git a/src/content/posts/bitcoin/ms-by-pool.json b/src/content/posts/bitcoin/ms-by-pool.json new file mode 100644 index 0000000..f59dfbd --- /dev/null +++ b/src/content/posts/bitcoin/ms-by-pool.json @@ -0,0 +1,534 @@ +{ + "Foundry USA": { + "months": { + "2023-12": 20870000, + "2024-01": 289390500, + "2024-02": 107362500, + "2024-03": 427250, + "2024-04": 35782000, + "2024-05": 1091500, + "2024-06": 1383775, + "2024-07": 6165950, + "2024-08": 14494500, + "2024-09": 865500, + "2024-10": 16328301, + "2024-11": 7020606, + "2024-12": 4387602, + "2025-01": 2266197, + "2025-02": 2093172, + "2025-03": 2854105, + "2025-04": 2264284, + "2025-05": 1536677, + "2025-06": 1180135, + "2025-07": 2712433, + "2025-08": 459454, + "2025-09": 538116, + "2025-10": 528157, + "2025-11": 383991, + "2025-12": 500000, + "2026-01": 674472, + "2026-02": 1790928, + "2026-03": 984285, + "2026-04": 1089833, + "2026-05": 1526329, + "2026-06": 73239643, + "2026-07": 1247857, + "2026-08": 1165252 + }, + "sum": 604605304 + }, + "AntPool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 9500, + "2024-09": 2919250, + "2024-10": 14715680, + "2024-11": 8014382, + "2024-12": 5590764, + "2025-01": 2182291, + "2025-02": 2210548, + "2025-03": 3763731, + "2025-04": 1856831, + "2025-05": 1685029, + "2025-06": 1811313, + "2025-07": 2369221, + "2025-08": 836454, + "2025-09": 605433, + "2025-10": 590000, + "2025-11": 374100, + "2025-12": 638977, + "2026-01": 580562, + "2026-02": 1888987, + "2026-03": 493644, + "2026-04": 413429, + "2026-05": 1062561, + "2026-06": 55095441, + "2026-07": 1422032, + "2026-08": 1091355 + }, + "sum": 112221515 + }, + "F2Pool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 131000, + "2024-09": 731750, + "2024-10": 5453604, + "2024-11": 4686959, + "2024-12": 3542128, + "2025-01": 1449809, + "2025-02": 1435272, + "2025-03": 1550967, + "2025-04": 1018941, + "2025-05": 705640, + "2025-06": 745445, + "2025-07": 596915, + "2025-08": 164001, + "2025-09": 328984, + "2025-10": 515639, + "2025-11": 370429, + "2025-12": 421794, + "2026-01": 282964, + "2026-02": 492044, + "2026-03": 301328, + "2026-04": 327986, + "2026-05": 259262, + "2026-06": 33702688, + "2026-07": 906318, + "2026-08": 635507 + }, + "sum": 60757374 + }, + "SpiderPool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 76000, + "2024-06": 288050, + "2024-07": 629675, + "2024-08": 485750, + "2024-09": 621250, + "2024-10": 3167230, + "2024-11": 1018232, + "2024-12": 417230, + "2025-01": 1019942, + "2025-02": 256753, + "2025-03": 236223, + "2025-04": 217285, + "2025-05": 524090, + "2025-06": 356462, + "2025-07": 3914637, + "2025-08": 638648, + "2025-09": 436135, + "2025-10": 145915, + "2025-11": 220850, + "2025-12": 294851, + "2026-01": 118300, + "2026-02": 208964, + "2026-03": 402817, + "2026-04": 216576, + "2026-05": 307604, + "2026-06": 23890000, + "2026-07": 1000773, + "2026-08": 158506 + }, + "sum": 41268748 + }, + "MARA Pool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 404250, + "2024-08": 1005750, + "2024-09": 1670500, + "2024-10": 3902345, + "2024-11": 310682, + "2024-12": 190139, + "2025-01": 110994, + "2025-02": 132128, + "2025-03": 686552, + "2025-04": 219197, + "2025-05": 1034262, + "2025-06": 178863, + "2025-07": 867399, + "2025-08": 98430, + "2025-09": 55005, + "2025-10": 24742, + "2025-11": 145979, + "2025-12": 107518, + "2026-01": 76542, + "2026-02": 80779, + "2026-03": 44244, + "2026-04": 73020, + "2026-05": 42996, + "2026-06": 7366645, + "2026-07": 108506, + "2026-08": 42943 + }, + "sum": 18980410 + }, + "Luxor": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 37750, + "2024-10": 1440809, + "2024-11": 920806, + "2024-12": 164280, + "2025-01": 364773, + "2025-02": 208174, + "2025-03": 310360, + "2025-04": 221303, + "2025-05": 670949, + "2025-06": 166496, + "2025-07": 106733, + "2025-08": 183169, + "2025-09": 97496, + "2025-10": 117233, + "2025-11": 147446, + "2025-12": 165922, + "2026-01": 84570, + "2026-02": 72864, + "2026-03": 69545, + "2026-04": 156462, + "2026-05": 102890, + "2026-06": 9638990, + "2026-07": 416136, + "2026-08": 147752 + }, + "sum": 16012908 + }, + "Braiins Pool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 485750, + "2024-10": 1669612, + "2024-11": 668212, + "2024-12": 366154, + "2025-01": 104725, + "2025-02": 421127, + "2025-03": 66356, + "2025-04": 96823, + "2025-05": 77196, + "2025-06": 64253, + "2025-07": 95365, + "2025-08": 123470, + "2025-09": 161971, + "2025-10": 54017, + "2025-11": 34944, + "2025-12": 14455, + "2026-01": 49505, + "2026-02": 118017, + "2026-03": 10618, + "2026-04": 9995, + "2026-05": 47041, + "2026-06": 7609621, + "2026-07": 19471, + "2026-08": 36014 + }, + "sum": 12404712 + }, + "Binance Pool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 331250, + "2024-10": 2388333, + "2024-11": 1086599, + "2024-12": 227218, + "2025-01": 106917, + "2025-02": 228560, + "2025-03": 295213, + "2025-04": 176287, + "2025-05": 110943, + "2025-06": 170246, + "2025-07": 102766, + "2025-08": 59482, + "2025-09": 78519, + "2025-10": 13984, + "2025-11": 58992, + "2025-12": 50123, + "2026-01": 58866, + "2026-02": 96688, + "2026-03": 18008, + "2026-04": 68237, + "2026-05": 21926, + "2026-06": 4460181, + "2026-07": 86443, + "2026-08": 114082 + }, + "sum": 10409863 + }, + "SBI Crypto": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 1875, + "2024-07": 215625, + "2024-08": 431500, + "2024-09": 812500, + "2024-10": 274805, + "2024-11": 228672, + "2024-12": 322918, + "2025-01": 41377, + "2025-02": 64587, + "2025-03": 127030, + "2025-04": 314408, + "2025-05": 368399, + "2025-06": 150946, + "2025-07": 95028, + "2025-08": 59000, + "2025-09": 192654, + "2025-10": 45498, + "2025-11": 30595, + "2025-12": 26502, + "2026-01": 20487, + "2026-02": 22737, + "2026-03": 99568, + "2026-04": 39693, + "2026-05": 95051, + "2026-06": 5616311, + "2026-07": 76128, + "2026-08": 0 + }, + "sum": 9773894 + }, + "Poolin": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 6000, + "2024-10": 1351936, + "2024-11": 30084, + "2024-12": 96390, + "2025-01": 21563, + "2025-02": 59256, + "2025-03": 101268, + "2025-04": 41337, + "2025-05": 70492, + "2025-06": 0, + "2025-07": 11023, + "2025-08": 2000, + "2025-09": 0, + "2025-10": 0, + "2025-11": 15000, + "2025-12": 10500, + "2026-01": 2002, + "2026-02": 1000, + "2026-03": 0, + "2026-04": 2000, + "2026-05": 0, + "2026-06": 1761149, + "2026-07": 7996, + "2026-08": 0 + }, + "sum": 3590996 + }, + "BTC.com": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 23750, + "2024-10": 392397, + "2024-11": 148066, + "2024-12": 235914, + "2025-01": 41685, + "2025-02": 70486, + "2025-03": 30380, + "2025-04": 95364, + "2025-05": 123958, + "2025-06": 39502, + "2025-07": 24081, + "2025-08": 38007, + "2025-09": 7000, + "2025-10": 5990, + "2025-11": 74498, + "2025-12": 19500, + "2026-01": 0, + "2026-02": 13961, + "2026-03": 2000, + "2026-04": 46997, + "2026-05": 0, + "2026-06": 1377470, + "2026-07": 104073, + "2026-08": 88528 + }, + "sum": 3003607 + }, + "WhitePool": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 3000, + "2024-10": 115499, + "2024-11": 97947, + "2024-12": 141354, + "2025-01": 90102, + "2025-02": 61639, + "2025-03": 30193, + "2025-04": 55508, + "2025-05": 14000, + "2025-06": 63514, + "2025-07": 95495, + "2025-08": 21995, + "2025-09": 59956, + "2025-10": 9995, + "2025-11": 30519, + "2025-12": 58474, + "2026-01": 2000, + "2026-02": 22480, + "2026-03": 0, + "2026-04": 5003, + "2026-05": 2001, + "2026-06": 388869, + "2026-07": 21462, + "2026-08": 7526 + }, + "sum": 1398531 + }, + "ULTIMUSPOOL": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 0, + "2024-08": 0, + "2024-09": 4000, + "2024-10": 421640, + "2024-11": 147046, + "2024-12": 19169, + "2025-01": 31515, + "2025-02": 16485, + "2025-03": 0, + "2025-04": 15498, + "2025-05": 30146, + "2025-06": 28943, + "2025-07": 18500, + "2025-08": 18489, + "2025-09": 25000, + "2025-10": 33999, + "2025-11": 0, + "2025-12": 0, + "2026-01": 17001, + "2026-02": 0, + "2026-03": 12000, + "2026-04": 4028, + "2026-05": 11015, + "2026-06": 471820, + "2026-07": 0, + "2026-08": 0 + }, + "sum": 1326294 + }, + "OCEAN": { + "months": { + "2023-12": 0, + "2024-01": 0, + "2024-02": 0, + "2024-03": 0, + "2024-04": 0, + "2024-05": 0, + "2024-06": 0, + "2024-07": 5500, + "2024-08": 3000, + "2024-09": 19250, + "2024-10": 12750, + "2024-11": 6272, + "2024-12": 0, + "2025-01": 2006, + "2025-02": 200032, + "2025-03": 41059, + "2025-04": 17156, + "2025-05": 13997, + "2025-06": 2000, + "2025-07": 0, + "2025-08": 0, + "2025-09": 0, + "2025-10": 0, + "2025-11": 0, + "2025-12": 0, + "2026-01": 0, + "2026-02": 0, + "2026-03": 0, + "2026-04": 0, + "2026-05": 0, + "2026-06": 0, + "2026-07": 0, + "2026-08": 0 + }, + "sum": 323022 + } +} diff --git a/src/lib/offchain-revenue-jan2023.json b/src/content/posts/bitcoin/offchain-revenue-jan2023.json similarity index 100% rename from src/lib/offchain-revenue-jan2023.json rename to src/content/posts/bitcoin/offchain-revenue-jan2023.json diff --git a/src/content/posts/prologue.mdx b/src/content/posts/prologue/index.mdx similarity index 100% rename from src/content/posts/prologue.mdx rename to src/content/posts/prologue/index.mdx diff --git a/src/layouts/PostLayout.astro b/src/layouts/PostLayout.astro index 559fd8f..69ac8e0 100644 --- a/src/layouts/PostLayout.astro +++ b/src/layouts/PostLayout.astro @@ -67,9 +67,11 @@ const headingClass =
diff --git a/src/lib/bitcoin-timeline.ts b/src/lib/bitcoin-timeline.ts index 9926525..80223ed 100644 --- a/src/lib/bitcoin-timeline.ts +++ b/src/lib/bitcoin-timeline.ts @@ -35,7 +35,7 @@ * of history — which also means nothing here is fetched at runtime, and no * figure on the chart can be unavailable. */ -import barData from "@/lib/bitcoin-bars.json"; +import barData from "@/content/posts/bitcoin/bars.json"; export interface Bar { /** The first block this bar covers; it covers `BLOCKS_PER_BAR` of them. */ diff --git a/src/lib/offchain-revenue.ts b/src/lib/offchain-revenue.ts index 0a8d7c3..a52cb7e 100644 --- a/src/lib/offchain-revenue.ts +++ b/src/lib/offchain-revenue.ts @@ -12,7 +12,7 @@ * listed; `totalBlocks` and `blocksPerDay` carry the blocks that were not * flagged, which is what the coverage rail is drawn from. */ -import data from "@/lib/offchain-revenue-jan2023.json"; +import data from "@/content/posts/bitcoin/offchain-revenue-jan2023.json"; export interface FlaggedBlock { /** Block height. */ From 561cfd4639462d1b017b325fcd5260a9f6f4dfbd Mon Sep 17 00:00:00 2001 From: 37ng Date: Tue, 8 Sep 2026 09:41:36 -0700 Subject: [PATCH 2/2] leftover commits --- CLAUDE.md | 16 ++++++++-------- package.json | 2 +- scripts/generate-art.mjs | 2 +- scripts/generate-bitcoin-epochs.mjs | 4 ++-- src/components/HeroStage.astro | 6 +++--- src/components/PostsFan.tsx | 2 +- src/content.config.ts | 2 +- .../{posts/bitcoin => bitcoin/data}/bars.json | 0 .../{posts/bitcoin => bitcoin/data}/epochs.json | 0 .../data/mempool-space-by-month.json} | 0 .../data/mempool-space-by-pool.json} | 0 .../data}/offchain-revenue-jan2023.json | 0 .../bitcoin}/diagrams/BitcoinCoin3D.tsx | 0 .../bitcoin}/diagrams/BitcoinTimeline.astro | 2 +- .../bitcoin}/diagrams/BitcoinTimeline.tsx | 0 .../bitcoin}/diagrams/FlagRateMempool.astro | 0 .../bitcoin}/diagrams/FlagRateMempool.tsx | 0 .../bitcoin}/diagrams/FlaggedBlocks.astro | 0 .../bitcoin}/diagrams/FlaggedBlocks.tsx | 0 .../bitcoin}/diagrams/OffchainRevenue.tsx | 0 .../bitcoin}/diagrams/PoolFlagHeatmap.astro | 0 .../bitcoin}/diagrams/PoolFlagHeatmap.tsx | 0 src/content/{posts => }/bitcoin/index.mdx | 16 +++++++++------- .../prologue}/PrologueSheet.tsx | 0 src/content/{posts => }/prologue/index.mdx | 0 src/lib/bitcoin-timeline.ts | 2 +- src/lib/offchain-revenue.ts | 2 +- 27 files changed, 29 insertions(+), 27 deletions(-) rename src/content/{posts/bitcoin => bitcoin/data}/bars.json (100%) rename src/content/{posts/bitcoin => bitcoin/data}/epochs.json (100%) rename src/content/{posts/bitcoin/ms-by-month.json => bitcoin/data/mempool-space-by-month.json} (100%) rename src/content/{posts/bitcoin/ms-by-pool.json => bitcoin/data/mempool-space-by-pool.json} (100%) rename src/content/{posts/bitcoin => bitcoin/data}/offchain-revenue-jan2023.json (100%) rename src/{components => content/bitcoin}/diagrams/BitcoinCoin3D.tsx (100%) rename src/{components => content/bitcoin}/diagrams/BitcoinTimeline.astro (88%) rename src/{components => content/bitcoin}/diagrams/BitcoinTimeline.tsx (100%) rename src/{components => content/bitcoin}/diagrams/FlagRateMempool.astro (100%) rename src/{components => content/bitcoin}/diagrams/FlagRateMempool.tsx (100%) rename src/{components => content/bitcoin}/diagrams/FlaggedBlocks.astro (100%) rename src/{components => content/bitcoin}/diagrams/FlaggedBlocks.tsx (100%) rename src/{components => content/bitcoin}/diagrams/OffchainRevenue.tsx (100%) rename src/{components => content/bitcoin}/diagrams/PoolFlagHeatmap.astro (100%) rename src/{components => content/bitcoin}/diagrams/PoolFlagHeatmap.tsx (100%) rename src/content/{posts => }/bitcoin/index.mdx (80%) rename src/{components/stages => content/prologue}/PrologueSheet.tsx (100%) rename src/content/{posts => }/prologue/index.mdx (100%) diff --git a/CLAUDE.md b/CLAUDE.md index f4b4eea..dc2082e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,9 @@ Prettier runs automatically on staged files via the `.githooks/pre-commit` hook. ## Website structure -- **The stage** (`src/components/HeroStage.astro`): one full-screen subject at a time — backdrop art, live furniture (the bitcoin coin), and the bottom-left caption, all belonging to the same post and swapping together. Every post in the fan has a stage; the prologue gets `stages/PrologueSheet.tsx`, a sheet of manila carrying a pencil signature (the same component draws the prologue's card thumbnail in the fan, so the two can never drift), and any other post with no `stageImage` gets a plain surface in its `stageTone` — and `index.astro` derives `light` for it rather than reading `stageTone`, since paper is always light and trusting the field there fails invisibly (white type on cream) instead of visibly. **The homepage does not scroll** — every layer is fixed to the viewport and the fan is the only way through the work; `LANDING_STAGE` in `index.astro` is what a first visit sees. Which subject is up is ``. Hovering a card is the only thing that writes it (`setStage`, `src/lib/stage.ts`) and **nothing clears it**: a stage stays up after the fan drops, and is saved to `sessionStorage` so opening a post and coming home returns you to it rather than to the landing stage. **Visibility is pure CSS, and it has to be** — the browser paints `HeroStage`'s markup long before the island hydrates, so a stage chosen in JSX shows the landing stage first and cross-fades off it on the way home. So `HeroStage.astro` renders every stage and holds no state — it is **not an island**; only `` inside it runs on the client. The per-id rules and the `` bootstrap that sets `data-stage` before the body is parsed are both **generated from the post list in `index.astro`** (adding a post needs no edit). Adding new per-stage furniture means writing another block in `HeroStage.astro` by hand: a client directive is compile-time, so it can't be looked up from a map the way the old React version did. Two traps if you touch that bootstrap: the storage key must stay in step with `stage.ts`, and it must stay one `var`-only IIFE — a view transition re-executes it in the same window, so a top-level `const` (what `define:vars` emits) throws on the second run. It also re-applies on `astro:after-swap`, since a swap replaces ``'s attributes. +- **The stage** (`src/components/HeroStage.astro`): one full-screen subject at a time — backdrop art, live furniture (the bitcoin coin), and the bottom-left caption, all belonging to the same post and swapping together. Every post in the fan has a stage; the prologue gets `src/content/prologue/PrologueSheet.tsx`, a sheet of manila carrying a pencil signature (the same component draws the prologue's card thumbnail in the fan, so the two can never drift — which is why it sits in the post folder even though only the stage and the fan import it, never the prose), and any other post with no `stageImage` gets a plain surface in its `stageTone` — and `index.astro` derives `light` for it rather than reading `stageTone`, since paper is always light and trusting the field there fails invisibly (white type on cream) instead of visibly. **The homepage does not scroll** — every layer is fixed to the viewport and the fan is the only way through the work; `LANDING_STAGE` in `index.astro` is what a first visit sees. Which subject is up is ``. Hovering a card is the only thing that writes it (`setStage`, `src/lib/stage.ts`) and **nothing clears it**: a stage stays up after the fan drops, and is saved to `sessionStorage` so opening a post and coming home returns you to it rather than to the landing stage. **Visibility is pure CSS, and it has to be** — the browser paints `HeroStage`'s markup long before the island hydrates, so a stage chosen in JSX shows the landing stage first and cross-fades off it on the way home. So `HeroStage.astro` renders every stage and holds no state — it is **not an island**; only `` inside it runs on the client. The per-id rules and the `` bootstrap that sets `data-stage` before the body is parsed are both **generated from the post list in `index.astro`** (adding a post needs no edit). Adding new per-stage furniture means writing another block in `HeroStage.astro` by hand: a client directive is compile-time, so it can't be looked up from a map the way the old React version did. Two traps if you touch that bootstrap: the storage key must stay in step with `stage.ts`, and it must stay one `var`-only IIFE — a view transition re-executes it in the same window, so a top-level `const` (what `define:vars` emits) throws on the second run. It also re-applies on `astro:after-swap`, since a swap replaces ``'s attributes. - **Stage → post**: clicking the stage caption opens that blog post, which has richer interactive animations per concept and an optional link to the project's GitHub repo. -- **Index**: `src/components/PostsFan.tsx` (`client:load`) — a hand of cream index cards fanned along the bottom edge of the landing page. It is furniture, not an overlay: `--fan-peek` of each card stays on screen at rest, and the hand rises when the pointer reaches it (or on focus, or on tap). Every card is a link — hovering raises that post onto the stage (`setStage`), which is also what lifts the card out of the hand; the fan keeps no selection state of its own. While the hand is up, the card's title and description are written centered above it and the stage caption steps aside — same content, only the position changes, so lowering the hand is a handover rather than a reset. Both write the same `N-00x · date` index line, so the handover carries the number too. That number is the post's **position in the fan's order**, which is why `CURATED_ORDER`/`sortCurated`/`cardNo` live in `src/lib/fan.ts` and not in the island: `index.astro` sorts the one list before handing it to both, so the static caption and the hydrated panel can never number a post differently. Card **N-000** is the prologue: not a bespoke component, just `src/content/posts/prologue/index.mdx` pinned first in `CURATED_ORDER`, same as every other post. Live per-stage furniture (the bitcoin coin) lives in `STAGE_FURNITURE` in `HeroStage.tsx`, mounted once into a fixed layer rather than inside a snap section: a card can raise it from any scroll position, and one WebGL context is enough. +- **Index**: `src/components/PostsFan.tsx` (`client:load`) — a hand of cream index cards fanned along the bottom edge of the landing page. It is furniture, not an overlay: `--fan-peek` of each card stays on screen at rest, and the hand rises when the pointer reaches it (or on focus, or on tap). Every card is a link — hovering raises that post onto the stage (`setStage`), which is also what lifts the card out of the hand; the fan keeps no selection state of its own. While the hand is up, the card's title and description are written centered above it and the stage caption steps aside — same content, only the position changes, so lowering the hand is a handover rather than a reset. Both write the same `N-00x · date` index line, so the handover carries the number too. That number is the post's **position in the fan's order**, which is why `CURATED_ORDER`/`sortCurated`/`cardNo` live in `src/lib/fan.ts` and not in the island: `index.astro` sorts the one list before handing it to both, so the static caption and the hydrated panel can never number a post differently. Card **N-000** is the prologue: not a bespoke component, just `src/content/prologue/index.mdx` pinned first in `CURATED_ORDER`, same as every other post. Live per-stage furniture (the bitcoin coin) lives in `STAGE_FURNITURE` in `HeroStage.tsx`, mounted once into a fixed layer rather than inside a snap section: a card can raise it from any scroll position, and one WebGL context is enough. - **Header**: site label on the left, live clock on the right. No nav links. ## Blog @@ -25,7 +25,7 @@ Each blog is an MDX file that combines article content with interactive illustra **Routing:** File-based. `src/pages/index.astro` is the homepage. `src/pages/blog/[slug].astro` is the dynamic blog route — it calls `getStaticPaths()` to pre-render one page per MDX post at build time. -**Content:** One folder per post under `src/content/posts//`, holding `index.mdx` plus that post's own data files (`bitcoin/bars.json`, `bitcoin/ms-by-pool.json`, …). The collection loader globs `**/*.mdx`, so the JSON sitting beside the prose is never mistaken for an entry, and `index` is stripped from the id — `posts/bitcoin/index.mdx` still serves `/bitcoin`. Post-scoped data is imported from there (`@/content/posts/bitcoin/bars.json`) rather than from `src/lib/`, which now holds only the logic and the site-wide modules. The schema (title, description, date, tags) is enforced in `src/content.config.ts` via Zod. Posts can import and render any React component inline. +**Content:** One folder per post under `src/content//`, holding `index.mdx`, that post's own data in `data/` (`bitcoin/data/bars.json`, `bitcoin/data/mempool-space-by-pool.json`, …) and its own figures in `diagrams/`. The collection loader globs `**/*.mdx`, so the JSON sitting beside the prose is never mistaken for an entry, and `index` is stripped from the id — `bitcoin/index.mdx` still serves `/bitcoin`. Post-scoped data is imported from there (`@/content/bitcoin/data/bars.json`) rather than from `src/lib/`, which now holds only the logic and the site-wide modules. The schema (title, description, date, tags) is enforced in `src/content.config.ts` via Zod. Posts can import and render any React component inline. **Per-post color — two axes, deliberately separate.** `color` in frontmatter is **identity**: one custom hue per post, free to be loud, never required to be legible over anything. It drives `--hero-accent` (the fan's `N-00x · date` line, the stage caption's hover) and the post page's own `--accent`. `stageTone: "dark" | "light"` is **legibility**: it declares whether that post's hero art is dark or light, and selects a whole set of readable tones — `--stage-title` and `--stage-body` for the caption, and the same `--stage-title` for the header chrome (site label + clock), which sits directly on the art. Both are emitted as per-id rules generated in `index.astro`; the light pair is named in `global.css` (`--stage-title-light`/`--stage-body-light`) because Tailwind drops any `@theme` token that nothing in that stylesheet references, and the generated CSS is invisible to it. A post that sets neither gets signal orange and the dark tones. @@ -35,19 +35,19 @@ Each blog is an MDX file that combines article content with interactive illustra **Design language:** "Engineering plan set / spec sheet." Cold blue-slate `ink` neutrals + one oxide-orange `signal` accent (`#c2410c`-family) — signal appears only on rules, indices, and hover states, never on body text. Mono (`font-mono`, JetBrains Mono) carries all UI chrome — nav, labels, dates — uppercase with wide tracking (enforced globally by a `.font-mono` rule in `global.css`, not per-component). Fraunces drives its variable axes (`opsz`/`SOFT`/`WONK`) on every heading, sitewide, not just in prose. Zero radius everywhere (`--radius-*: 0`); panels use hairline rules (`shadow-panel` token), not blurred drop shadows. Interactions are mechanical — no opacity fades or translate-Y lifts; see `.spec-card` (hard offset border, print-misregistration style) and `.nav-index` (mono index number that shutters open, not fades in) in `global.css`. Apply `prose-tech` class to article content for the blog prose styles — includes auto-numbered `§01`/`§02` section counters on `h2`. Every `h2`/`h3` also gets a hover-revealed `#` permalink (`.heading-anchor`, injected at build time by `rehype-autolink-headings` in `astro.config.mjs`). -**Onchain revenue timeline** (`src/components/diagrams/BitcoinTimeline.tsx`, on the bitcoin stage and in `bitcoin.mdx`): one bar per period, and every figure is a **period total** — what the chain earned over the whole bar, roughly a month — never an average per block, since dividing by however many blocks were mined is the one part of the question nobody asked. The bar is **tx fees only**, priced in **dollars of the day**: a fee in BTC says nothing across seventeen years, because the unit itself changed value ten-thousandfold along the axis it is plotted against, so each row carries its own BTC/USD and a 2012 month is priced at 2012 rates. The x axis is **block height, not the calendar**: one bar per **4,375 blocks**, which is 210,000 / 48, so a halving is always a bar _edge_ and never a point inside a bar (a calendar month would drift across boundaries). Subsidy is never stored — `subsidyAt(height)` is `50 / 2 ** floor(h / 210_000)`, and `subsidyBtcOverBar` scales it to the bar so it can be added to a period total — and neither is the dollar figure, which is one multiplication away from the price each row carries (`usdWorth`). The log scale is floored at a dollar (`MIN_USD`) — a 2009 month of fees is worth hundred-thousandths of a cent, and letting that end in stretches the scale over thirteen orders and pins everything after 2012 to the top. `src/content/posts/bitcoin/bars.json` is **pseudo data**: the heights are exact and the month labels follow the real halving dates, but the fees and the price are invented. There is **no generator script** for it — replace the file with real per-bar measurements. Reading is by hover only: the bar under the pointer is redrawn in the accent — one mark, the same colour as the halving tick and the axis cursor — and the cursor stays where the pointer left rather than snapping back to the last bar. It also carries the **annualized percentage**: a year of that bar's pay over every coin issued (`onchainShare`, annualized by `BARS_PER_YEAR` since the rows are period totals), with `supplyAt` walking the halving schedule the way `subsidyAt` does and `supplyAfter` supplying an end-of-bar denominator that is never zero. Both sides are BTC, so the price cancels and that column owes nothing to the price beside it. Nothing animates, and nothing is fetched at runtime (an earlier version grew the bars in on a `requestAnimationFrame`, which never fires in a hidden tab and left the chart at `scaleY(0)`). +**Onchain revenue timeline** (`src/content/bitcoin/diagrams/BitcoinTimeline.tsx`, on the bitcoin stage and in the post): one bar per period, and every figure is a **period total** — what the chain earned over the whole bar, roughly a month — never an average per block, since dividing by however many blocks were mined is the one part of the question nobody asked. The bar is **tx fees only**, priced in **dollars of the day**: a fee in BTC says nothing across seventeen years, because the unit itself changed value ten-thousandfold along the axis it is plotted against, so each row carries its own BTC/USD and a 2012 month is priced at 2012 rates. The x axis is **block height, not the calendar**: one bar per **4,375 blocks**, which is 210,000 / 48, so a halving is always a bar _edge_ and never a point inside a bar (a calendar month would drift across boundaries). Subsidy is never stored — `subsidyAt(height)` is `50 / 2 ** floor(h / 210_000)`, and `subsidyBtcOverBar` scales it to the bar so it can be added to a period total — and neither is the dollar figure, which is one multiplication away from the price each row carries (`usdWorth`). The log scale is floored at a dollar (`MIN_USD`) — a 2009 month of fees is worth hundred-thousandths of a cent, and letting that end in stretches the scale over thirteen orders and pins everything after 2012 to the top. `src/content/bitcoin/data/bars.json` is **pseudo data**: the heights are exact and the month labels follow the real halving dates, but the fees and the price are invented. There is **no generator script** for it — replace the file with real per-bar measurements. Reading is by hover only: the bar under the pointer is redrawn in the accent — one mark, the same colour as the halving tick and the axis cursor — and the cursor stays where the pointer left rather than snapping back to the last bar. It also carries the **annualized percentage**: a year of that bar's pay over every coin issued (`onchainShare`, annualized by `BARS_PER_YEAR` since the rows are period totals), with `supplyAt` walking the halving schedule the way `subsidyAt` does and `supplyAfter` supplying an end-of-bar denominator that is never zero. Both sides are BTC, so the price cancels and that column owes nothing to the price beside it. Nothing animates, and nothing is fetched at runtime (an earlier version grew the bars in on a `requestAnimationFrame`, which never fires in a hidden tab and left the chart at `scaleY(0)`). -**Implied off-chain revenue figure** (`src/components/diagrams/OffchainRevenue.tsx`, in `bitcoin.mdx`): one mark per block that mined a transaction paying under its own floor feerate, `(floor_B − actual) × vsize` on a log scale, with a per-day coverage rail underneath for the blocks that carried nothing. The numbers in `src/content/posts/bitcoin/offchain-revenue-jan2023.json` are **pseudo data** — a deterministic stand-in written by `npm run offchain-pseudo`, there so the figure could be built before the real measurements exist. Real data replaces that one file and nothing else; the generator script then goes away. Two things the generator has to keep true, because the tooltip prints all three numbers side by side: `impliedSats` is derived from the _rounded_ feerates, so a reader who multiplies them out lands on the printed figure; and only flagged blocks are listed, with `totalBlocks`/`blocksPerDay` carrying the rest (the rail's denominator). Marks are painted through `.chart-mark` in `global.css` rather than a token utility — a post's `color` is chosen for identity, not contrast, so on the light theme's paper the figure swaps to `--accent-dark`. +**Implied off-chain revenue figure** (`src/content/bitcoin/diagrams/OffchainRevenue.tsx`, in the post): one mark per block that mined a transaction paying under its own floor feerate, `(floor_B − actual) × vsize` on a log scale, with a per-day coverage rail underneath for the blocks that carried nothing. The numbers in `src/content/bitcoin/data/offchain-revenue-jan2023.json` are **pseudo data** — a deterministic stand-in written by `npm run offchain-pseudo`, there so the figure could be built before the real measurements exist. Real data replaces that one file and nothing else; the generator script then goes away. Two things the generator has to keep true, because the tooltip prints all three numbers side by side: `impliedSats` is derived from the _rounded_ feerates, so a reader who multiplies them out lands on the printed figure; and only flagged blocks are listed, with `totalBlocks`/`blocksPerDay` carrying the rest (the rail's denominator). Marks are painted through `.chart-mark` in `global.css` rather than a token utility — a post's `color` is chosen for identity, not contrast, so on the light theme's paper the figure swaps to `--accent-dark`. -**Interactive diagrams:** `src/components/diagrams/` holds heavier React components (e.g. `BitcoinCoin3D.tsx`, `three` + `@react-three/fiber`). They must be hydrated explicitly at the call site — `client:load` in MDX posts, `client:idle` for the stage furniture in `HeroStage.astro`. Their binary payloads live in that post's own `src/assets//` folder and are pulled in with a `?url` import (`import modelUrl from "@/assets/bitcoin/coin.glb?url"`), not from `public/`: the build then fingerprints the file for long-term caching and fails at compile time if it is ever moved or renamed, where a `public/` path only 404s at runtime. +**Interactive diagrams:** a post's own figures live in `src/content//diagrams/`, beside the prose and the data they read, so the MDX imports them relatively (`./diagrams/BitcoinTimeline.astro`) and `src/components/` keeps only what the whole site uses. They are heavier React components (e.g. `BitcoinCoin3D.tsx`, `three` + `@react-three/fiber`) and must be hydrated explicitly at the call site — `client:load`/`client:visible` in MDX posts, `client:idle` for the stage furniture in `HeroStage.astro`. A diagram with two homes gets a thin `.astro` mount point beside it (`BitcoinTimeline.astro`) so the hydration directive stays one decision; that is also the only reason `HeroStage.astro` reaches across into a post folder (`@/content/bitcoin/diagrams/…`) — the coin and the timeline belong to the bitcoin post, the stage only borrows them. Their binary payloads live in that post's own `src/assets//` folder and are pulled in with a `?url` import (`import modelUrl from "@/assets/bitcoin/coin.glb?url"`), not from `public/`: the build then fingerprints the file for long-term caching and fails at compile time if it is ever moved or renamed, where a `public/` path only 404s at runtime. -**Path alias:** `@/` maps to `src/` (used in MDX imports like `import { FlowDiagram } from '@/components/diagrams/FlowDiagram'`). +**Path alias:** `@/` maps to `src/` (used for site-wide modules like `import { cardNo } from '@/lib/fan'`; a post's own files are imported relatively instead). ## Adding content **Images.** Masters live in `art-src/`, which is **gitignored** — a multi-megabyte master committed once is in git history forever, and a second version doubles it. `npm run art` (`scripts/generate-art.mjs`) writes one stage-width webp per master into `src/assets/`, and _that_ is what gets committed (7 MB PNG → 696 KB webp). It mirrors subfolders, so the master for a post's stage art belongs at `art-src//.png` and lands at `src/assets//.webp`, one folder per post. From there the build behaves normally: `index.astro` derives the fan's 480px card thumbnail with `getImage()`, while the stage uses the committed file as-is — re-encoding a webp that is already the right size and format buys nothing. `--quality` (default 90) and `--sizes` (default 2560) are flags on the script. -**New blog post:** Create `src/content/posts/my-post/index.mdx` with frontmatter, and keep that post's data files and stage art in the same-named folders (`src/content/posts/my-post/`, `src/assets/my-post/`): +**New blog post:** Create `src/content/my-post/index.mdx` with frontmatter, and keep that post's data files and stage art in the same-named folders (`src/content/my-post/`, `src/assets/my-post/`): ```mdx --- diff --git a/package.json b/package.json index 7500d40..a1e9ab4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "verify": "npm run format:check && npm run check && npm run build && npm test", "art": "node scripts/generate-art.mjs", "bitcoin-epochs": "node scripts/generate-bitcoin-epochs.mjs", - "offchain-pseudo": "node scripts/generate-offchain-pseudo.mjs src/content/posts/bitcoin/offchain-revenue-jan2023.json", + "offchain-pseudo": "node scripts/generate-offchain-pseudo.mjs src/content/bitcoin/data/offchain-revenue-jan2023.json", "test": "tsx --test src/**/*.test.ts" }, "dependencies": { diff --git a/scripts/generate-art.mjs b/scripts/generate-art.mjs index 06c1c0b..75a741f 100644 --- a/scripts/generate-art.mjs +++ b/scripts/generate-art.mjs @@ -2,7 +2,7 @@ * Turns a full-size master in `art-src/` into the one committed webp the site * builds from, in `src/assets/`. Subdirectories are mirrored, so a master at * `art-src/bitcoin/bg.png` becomes `src/assets/bitcoin/bg.webp` — one folder - * per post, matching `src/content/posts/`. + * per post, matching `src/content/`. * * Why this exists rather than handing the master to Astro: a master is tens of * megabytes of lossless pixels, and committing one puts it in git history diff --git a/scripts/generate-bitcoin-epochs.mjs b/scripts/generate-bitcoin-epochs.mjs index 4534fef..b230679 100644 --- a/scripts/generate-bitcoin-epochs.mjs +++ b/scripts/generate-bitcoin-epochs.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Updates src/content/posts/bitcoin/epochs.json — one row per *finished* halving epoch, + * Updates src/content/bitcoin/data/epochs.json — one row per *finished* halving epoch, * aggregated from public history, plus the average BTC/USD needed to say what * that epoch's own tx fees were worth at the time. * @@ -45,7 +45,7 @@ import { access, readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const OUT = fileURLToPath( - new URL("../src/content/posts/bitcoin/epochs.json", import.meta.url), + new URL("../src/content/bitcoin/data/epochs.json", import.meta.url), ); // The protocol's own constants — a halving every 210,000 blocks, subsidy // halving each time from 50 BTC. Deriving epoch heights and subsidies from diff --git a/src/components/HeroStage.astro b/src/components/HeroStage.astro index 53d8901..67701ea 100644 --- a/src/components/HeroStage.astro +++ b/src/components/HeroStage.astro @@ -1,7 +1,7 @@ --- -import { BitcoinCoin3D } from "./diagrams/BitcoinCoin3D"; -import BitcoinTimeline from "./diagrams/BitcoinTimeline.astro"; -import { PrologueSheet } from "./stages/PrologueSheet"; +import { BitcoinCoin3D } from "@/content/bitcoin/diagrams/BitcoinCoin3D"; +import BitcoinTimeline from "@/content/bitcoin/diagrams/BitcoinTimeline.astro"; +import { PrologueSheet } from "@/content/prologue/PrologueSheet"; import { backdropFor, type StageSummary } from "@/lib/stage"; import { cardNo } from "@/lib/fan"; diff --git a/src/components/PostsFan.tsx b/src/components/PostsFan.tsx index 5fc7959..67bf05e 100644 --- a/src/components/PostsFan.tsx +++ b/src/components/PostsFan.tsx @@ -12,7 +12,7 @@ import { subscribeStage, } from "@/lib/stage"; import { cardNo, sortCurated } from "@/lib/fan"; -import { PrologueSheet } from "@/components/stages/PrologueSheet"; +import { PrologueSheet } from "@/content/prologue/PrologueSheet"; export interface PostSummary { id: string; diff --git a/src/content.config.ts b/src/content.config.ts index 99728f1..13c35d6 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -3,7 +3,7 @@ import { glob } from "astro/loaders"; import { z } from "astro/zod"; const posts = defineCollection({ - loader: glob({ pattern: "**/*.mdx", base: "./src/content/posts" }), + loader: glob({ pattern: "**/*.mdx", base: "./src/content" }), schema: ({ image }) => z.object({ title: z.string(), diff --git a/src/content/posts/bitcoin/bars.json b/src/content/bitcoin/data/bars.json similarity index 100% rename from src/content/posts/bitcoin/bars.json rename to src/content/bitcoin/data/bars.json diff --git a/src/content/posts/bitcoin/epochs.json b/src/content/bitcoin/data/epochs.json similarity index 100% rename from src/content/posts/bitcoin/epochs.json rename to src/content/bitcoin/data/epochs.json diff --git a/src/content/posts/bitcoin/ms-by-month.json b/src/content/bitcoin/data/mempool-space-by-month.json similarity index 100% rename from src/content/posts/bitcoin/ms-by-month.json rename to src/content/bitcoin/data/mempool-space-by-month.json diff --git a/src/content/posts/bitcoin/ms-by-pool.json b/src/content/bitcoin/data/mempool-space-by-pool.json similarity index 100% rename from src/content/posts/bitcoin/ms-by-pool.json rename to src/content/bitcoin/data/mempool-space-by-pool.json diff --git a/src/content/posts/bitcoin/offchain-revenue-jan2023.json b/src/content/bitcoin/data/offchain-revenue-jan2023.json similarity index 100% rename from src/content/posts/bitcoin/offchain-revenue-jan2023.json rename to src/content/bitcoin/data/offchain-revenue-jan2023.json diff --git a/src/components/diagrams/BitcoinCoin3D.tsx b/src/content/bitcoin/diagrams/BitcoinCoin3D.tsx similarity index 100% rename from src/components/diagrams/BitcoinCoin3D.tsx rename to src/content/bitcoin/diagrams/BitcoinCoin3D.tsx diff --git a/src/components/diagrams/BitcoinTimeline.astro b/src/content/bitcoin/diagrams/BitcoinTimeline.astro similarity index 88% rename from src/components/diagrams/BitcoinTimeline.astro rename to src/content/bitcoin/diagrams/BitcoinTimeline.astro index 1a270ca..fdccf61 100644 --- a/src/components/diagrams/BitcoinTimeline.astro +++ b/src/content/bitcoin/diagrams/BitcoinTimeline.astro @@ -4,7 +4,7 @@ * * Both homes — the bitcoin stage and the bitcoin post — go through this rather * than the island directly, so the hydration directive stays one decision - * instead of two. Every figure is static, read from `src/lib/bitcoin-months.json` + * instead of two. Every figure is static, read from `data/bars.json` beside it * at build time — nothing is fetched in the browser. */ import { BitcoinTimeline } from "./BitcoinTimeline"; diff --git a/src/components/diagrams/BitcoinTimeline.tsx b/src/content/bitcoin/diagrams/BitcoinTimeline.tsx similarity index 100% rename from src/components/diagrams/BitcoinTimeline.tsx rename to src/content/bitcoin/diagrams/BitcoinTimeline.tsx diff --git a/src/components/diagrams/FlagRateMempool.astro b/src/content/bitcoin/diagrams/FlagRateMempool.astro similarity index 100% rename from src/components/diagrams/FlagRateMempool.astro rename to src/content/bitcoin/diagrams/FlagRateMempool.astro diff --git a/src/components/diagrams/FlagRateMempool.tsx b/src/content/bitcoin/diagrams/FlagRateMempool.tsx similarity index 100% rename from src/components/diagrams/FlagRateMempool.tsx rename to src/content/bitcoin/diagrams/FlagRateMempool.tsx diff --git a/src/components/diagrams/FlaggedBlocks.astro b/src/content/bitcoin/diagrams/FlaggedBlocks.astro similarity index 100% rename from src/components/diagrams/FlaggedBlocks.astro rename to src/content/bitcoin/diagrams/FlaggedBlocks.astro diff --git a/src/components/diagrams/FlaggedBlocks.tsx b/src/content/bitcoin/diagrams/FlaggedBlocks.tsx similarity index 100% rename from src/components/diagrams/FlaggedBlocks.tsx rename to src/content/bitcoin/diagrams/FlaggedBlocks.tsx diff --git a/src/components/diagrams/OffchainRevenue.tsx b/src/content/bitcoin/diagrams/OffchainRevenue.tsx similarity index 100% rename from src/components/diagrams/OffchainRevenue.tsx rename to src/content/bitcoin/diagrams/OffchainRevenue.tsx diff --git a/src/components/diagrams/PoolFlagHeatmap.astro b/src/content/bitcoin/diagrams/PoolFlagHeatmap.astro similarity index 100% rename from src/components/diagrams/PoolFlagHeatmap.astro rename to src/content/bitcoin/diagrams/PoolFlagHeatmap.astro diff --git a/src/components/diagrams/PoolFlagHeatmap.tsx b/src/content/bitcoin/diagrams/PoolFlagHeatmap.tsx similarity index 100% rename from src/components/diagrams/PoolFlagHeatmap.tsx rename to src/content/bitcoin/diagrams/PoolFlagHeatmap.tsx diff --git a/src/content/posts/bitcoin/index.mdx b/src/content/bitcoin/index.mdx similarity index 80% rename from src/content/posts/bitcoin/index.mdx rename to src/content/bitcoin/index.mdx index 4878875..34531a9 100644 --- a/src/content/posts/bitcoin/index.mdx +++ b/src/content/bitcoin/index.mdx @@ -4,18 +4,18 @@ description: "Pools, farms and miners" date: "2026-07-28" feat: true tags: ["bitcoin", "crypto", "cryptocurrency"] -stageImage: "../../../assets/bitcoin/bg.webp" +stageImage: "../../assets/bitcoin/bg.webp" color: "#F7941D" stageTone: "dark" --- -import BitcoinTimeline from "@/components/diagrams/BitcoinTimeline.astro"; -import FlagRateMempool from "@/components/diagrams/FlagRateMempool.astro"; -import FlaggedBlocks from "@/components/diagrams/FlaggedBlocks.astro"; -import PoolFlagHeatmap from "@/components/diagrams/PoolFlagHeatmap.astro"; -import { OffchainRevenue } from "@/components/diagrams/OffchainRevenue"; +import BitcoinTimeline from "./diagrams/BitcoinTimeline.astro"; +import FlagRateMempool from "./diagrams/FlagRateMempool.astro"; +import FlaggedBlocks from "./diagrams/FlaggedBlocks.astro"; +import PoolFlagHeatmap from "./diagrams/PoolFlagHeatmap.astro"; +import { OffchainRevenue } from "./diagrams/OffchainRevenue"; + - Bitcoin's subsidy havles every 210000 blocks(~4 years). By the time of 33 halving(~year 2140), it will shrink to 0. Then transaction fee will be the only compensation for maintaining the network. @@ -65,3 +65,5 @@ The numbers here are placeholder data. + + diff --git a/src/components/stages/PrologueSheet.tsx b/src/content/prologue/PrologueSheet.tsx similarity index 100% rename from src/components/stages/PrologueSheet.tsx rename to src/content/prologue/PrologueSheet.tsx diff --git a/src/content/posts/prologue/index.mdx b/src/content/prologue/index.mdx similarity index 100% rename from src/content/posts/prologue/index.mdx rename to src/content/prologue/index.mdx diff --git a/src/lib/bitcoin-timeline.ts b/src/lib/bitcoin-timeline.ts index 80223ed..15e81e1 100644 --- a/src/lib/bitcoin-timeline.ts +++ b/src/lib/bitcoin-timeline.ts @@ -35,7 +35,7 @@ * of history — which also means nothing here is fetched at runtime, and no * figure on the chart can be unavailable. */ -import barData from "@/content/posts/bitcoin/bars.json"; +import barData from "@/content/bitcoin/data/bars.json"; export interface Bar { /** The first block this bar covers; it covers `BLOCKS_PER_BAR` of them. */ diff --git a/src/lib/offchain-revenue.ts b/src/lib/offchain-revenue.ts index a52cb7e..7d1a175 100644 --- a/src/lib/offchain-revenue.ts +++ b/src/lib/offchain-revenue.ts @@ -12,7 +12,7 @@ * listed; `totalBlocks` and `blocksPerDay` carry the blocks that were not * flagged, which is what the coverage rail is drawn from. */ -import data from "@/content/posts/bitcoin/offchain-revenue-jan2023.json"; +import data from "@/content/bitcoin/data/offchain-revenue-jan2023.json"; export interface FlaggedBlock { /** Block height. */