diff --git a/.gitignore b/.gitignore index 02104a3..f4cbd96 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ public/ result result-* .direnv/ + +# Zola class-based syntax highlighting — regenerated into static/ on every build +static/giallo-*.css diff --git a/CLAUDE.md b/CLAUDE.md index 43413bf..daeb9c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,13 @@ Project-level guidance for Claude Code working in this repo. ## Stack -Zola static site generator. Content lives in `content/`, templates in `templates/`, styles in `sass/main.scss`, structured data in `data/`. No npm. Build with `zola build`; serve locally via the launch config in `.claude/launch.json`. +Zola static site generator. Content lives in `content/`, templates in `templates/`, styles in `sass/main.scss`, structured data in `data/`. No npm. + +The toolchain is pinned by the Nix flake (ADR 0007), and config lives in `zola.toml` (a non-default name), so prefer the flake entrypoints over bare `zola`: + +- `nix build` — renders the site into `./result` (the rendered `public/` contents). +- `nix run` (alias `nix run .#serve`) — starts the live-reloading dev server (`zola serve`) at `http://127.0.0.1:1111`. +- `nix develop` — a shell with the pinned Zola on `PATH` for ad-hoc commands (pass `--config zola.toml`). ## Design rules diff --git a/README.md b/README.md index e967654..e25f9b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# shanemurphy.space +# semurphy.com Personal portfolio and writing site for Shane Murphy. Built with [Zola](https://www.getzola.org/) and deployed to GitHub Pages. @@ -17,19 +17,27 @@ No Node.js, no npm, no JavaScript build tooling. ## Local development -Install Zola via Homebrew: +The toolchain is pinned by the Nix flake (see [ADR 0007](docs/adr/0007-nix-build-tooling.md)) — no separate Zola install needed. + +Serve locally with live reload: ```bash -brew install zola +nix run ``` -Serve locally with live reload: +This runs `zola serve` with the correct config; the site is available at `http://127.0.0.1:1111`. + +Build the site into `./result` (the rendered `public/` contents): ```bash -zola serve +nix build ``` -The site is available at `http://127.0.0.1:1111`. +Or drop into a shell with the pinned Zola on `PATH` for ad-hoc commands: + +```bash +nix develop +``` ## Deployment @@ -40,12 +48,12 @@ Pushes to `main` automatically trigger the GitHub Actions workflow at `.github/w In the repository's **Settings → Pages**: - **Source:** GitHub Actions -- **Custom domain:** `shanemurphy.space` +- **Custom domain:** `semurphy.com` - **Enforce HTTPS:** enabled (once DNS has propagated) ## Custom domain DNS (Squarespace) -The domain `shanemurphy.space` is registered through Squarespace Domains. The following DNS records must be set in the Squarespace DNS panel: +The domain `semurphy.com` is registered through Squarespace Domains. The following DNS records must be set in the Squarespace DNS panel: ### A records (root domain) diff --git a/content/work/fitness.md b/content/work/fitness.md new file mode 100644 index 0000000..23dfbf3 --- /dev/null +++ b/content/work/fitness.md @@ -0,0 +1,5 @@ ++++ +title = "Why Sitting is Hard" +date = 2026-04-06 +draft = true ++++ diff --git a/content/work/virtues.md b/content/work/virtues.md new file mode 100644 index 0000000..d0f2a6a --- /dev/null +++ b/content/work/virtues.md @@ -0,0 +1,81 @@ ++++ +title = "Virtue-Driven Development Considered Harmful" +date = 2026-06-18 +draft = true ++++ + +Software teams love virtues. But architectural values are not ideals; they are +bundles of trade-offs sold under a single flattering name. + +- We chose Go because it's simple +- We reached for microservices because they scale +- We picked Rust because it's safe + +These sound like arguments, but they are slogans until they say what they cost. +And some don't even survive the asking. "Simplicity" is rarely created or +destroyed — just moved somewhere you stop looking at it. "Scalability" is a bill +you may have no reason to pay. + +Safety is the interesting one, because it survives. The borrow checker is a +genuine cost, paid in real units, and it still clears — which is exactly why it +makes a good ruler for the others. + + diff --git a/docs/adr/0008-theming-and-dark-mode.md b/docs/adr/0008-theming-and-dark-mode.md new file mode 100644 index 0000000..5e706e3 --- /dev/null +++ b/docs/adr/0008-theming-and-dark-mode.md @@ -0,0 +1,40 @@ +# 0008. Theming and dark mode + +- **Status:** Accepted +- **Date:** 2026-06-20 + +## Context + +The site shipped with a single light palette ("Birch & Cream") defined once in `sass/_palette.scss` and exposed to every component as `--color-*` custom properties. No dark mode existed. + +Adding a dark mode raises three questions: + +1. **How is a palette represented?** The CSS-custom-property indirection already in place means a theme is nothing more than a re-binding of the six `--color-*` tokens. No component CSS needs to change. This is the single-source-of-truth principle from CLAUDE.md applied to color. +2. **How does a visitor select a theme, and does an OS preference participate?** Options: manual toggle only; OS preference only (`prefers-color-scheme`); or both, with manual choice overriding the OS default. +3. **How is a flash of the wrong palette on load avoided?** A purely CSS/`prefers-color-scheme` solution flashes nothing, but a stored manual choice that contradicts the OS will flash unless applied before first paint. + +A dark palette was chosen by comparing three candidates in-browser ("Espresso" warm, "Slate" cool-neutral, "Ink" near-black). Slate was selected. + +## Decision + +**Palette = token re-binding.** Light stays the `:root` default. The dark palette ("Slate") is defined once as a Sass mixin (`slate-tokens`) and applied in two places: under `[data-theme="dark"]` (explicit choice) and under `:root:not([data-theme="light"])` inside a `@media (prefers-color-scheme: dark)` block (OS default). `color-scheme` is set alongside so native form controls and scrollbars match. + +**Resolution order:** explicit `[data-theme]` (set by the toggle) → OS `prefers-color-scheme` → light. A stored `"light"` choice opts back out of the OS default via the `:not([data-theme="light"])` guard. + +**Persistence and no-flash:** the chosen theme is stored in `localStorage` under `theme`. A tiny inline script in `` applies an explicit stored choice to `` before first paint. With no stored choice, nothing is applied and CSS `prefers-color-scheme` governs — so first-time visitors get their OS preference with zero flash. A second script wires the nav toggle button and keeps its label in sync when the OS preference changes and no explicit choice is set. + +Rejected: OS-preference-only (no way to override per-device); manual-only (ignores a preference the visitor already expressed at the OS level); a build-time or server-rendered theme (static host, no per-request state). + +## Consequences + +**Good:** +- Adding or swapping a palette is editing six values in one mixin; no component CSS changes. +- First-time visitors get their OS preference automatically; returning visitors get their explicit choice; neither flashes. +- The only JavaScript on the site is two small inline scripts — no framework, no build step, consistent with the no-npm constraint. + +**Bad / costs:** +- Two inline scripts in `base.html` are now load-bearing for correct first-paint behavior. They must stay inline (an external file would defeat the no-flash goal). +- Syntax highlighting is baked in at build time via `zola.toml`'s `catppuccin-latte` (a light theme). In dark mode, highlighted code blocks look out of place. Fixing this requires switching Zola to CSS-class highlighting and shipping per-theme highlight CSS — deferred until a writing post with code actually needs it. + +**Foreclosed:** +- A third user-selectable theme would reintroduce the cycle-style toggle the evaluation phase used; the current toggle is deliberately binary. diff --git a/docs/adr/0009-apex-domain-migration.md b/docs/adr/0009-apex-domain-migration.md new file mode 100644 index 0000000..9dcec40 --- /dev/null +++ b/docs/adr/0009-apex-domain-migration.md @@ -0,0 +1,34 @@ +# 0009. Apex domain migration to semurphy.com + +- **Status:** Accepted +- **Date:** 2026-06-20 + +## Context + +The site originally launched on `shanemurphy.space` (ADR 0001 hosting, ADR 0002 domain strategy). In practice the `.space` TLD caused friction: several services (form providers, some email validators, link unfurlers, and account-signup allowlists) treat newer/non-`.com` TLDs as suspicious or reject them outright. A shorter, conventional `.com` avoids this class of papercut. + +`semurphy.com` was acquired and the apex was migrated (CNAME + `base_url` already point at it; HTTPS live). This ADR records the decision so the architectural changelog stays coherent — `docs/roadmap.md` and `README.md` now say `semurphy.com`, which would otherwise silently contradict ADRs 0001–0002. + +## Decision + +The canonical apex domain is **`semurphy.com`**, registered through Squarespace Domains, served by GitHub Pages with the same A records and `www` CNAME (`shaneeverittm.github.io`) documented in `README.md`. `static/CNAME` and `zola.toml`'s `base_url` reflect this. + +This **amends the naming** used in ADRs 0001 and 0002; it does **not reverse their decisions**: + +- ADR 0001 (GitHub Pages hosting) stands unchanged. +- ADR 0002 (tracker on a subdomain, not a subpath) stands — the subdomain is now `tracker.semurphy.com`. + +The old `shanemurphy.space` continues to resolve and redirects to the canonical apex, so existing links don't break. + +## Consequences + +**Good:** +- Conventional `.com` removes TLD-based rejection from third-party services. +- No structural change: same host, same DNS shape, same deploy pipeline. + +**Bad / costs:** +- Two registrations to keep renewed for as long as the `.space` redirect is maintained. +- Any hard-coded `shanemurphy.space` strings (e.g. the contact email `mail@shanemurphy.space` in `zola.toml`) must be migrated deliberately — email in particular depends on where the mailbox actually lives and is intentionally left for a separate decision. + +**Foreclosed:** +- Nothing. The redirect preserves the old domain's inbound links indefinitely if desired. diff --git a/docs/adr/README.md b/docs/adr/README.md index ffa519f..332d57b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -27,3 +27,5 @@ A starter template is at `0000-template.md`. Copy it, renumber, fill it in. | [0005](0005-progression-rule-encoding.md) | Encoding progression rules in the program TOML | Accepted | | [0006](0006-session-json-schema.md) | Session JSON schema | Accepted | | [0007](0007-nix-build-tooling.md) | Nix flake as the build toolchain | Accepted | +| [0008](0008-theming-and-dark-mode.md) | Theming and dark mode | Accepted | +| [0009](0009-apex-domain-migration.md) | Apex domain migration to semurphy.com | Accepted | diff --git a/docs/roadmap.md b/docs/roadmap.md index b7b9814..eb45041 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,7 +5,7 @@ Living document. Updated as phases complete or assumptions change. The original ## Context -The site is a Zola SSG at shanemurphy.space (GitHub Pages, GitHub Actions deploy). It includes a workout program in +The site is a Zola SSG at semurphy.com (GitHub Pages, GitHub Actions deploy). It includes a workout program in `data/workout_program.toml` with auto-derived volume targets per muscle group. The goal is a workout tracker that: @@ -47,7 +47,7 @@ These touch every later phase, so resolving them first is load-bearing. Each wil 1. **Hosting target.** Currently on GitHub Pages. The tracker write path needs a serverless function that can commit to the repo via the GitHub API. -2. **Domain strategy.** Subdomain (`tracker.shanemurphy.space`) vs subpath (`/tracker`). +2. **Domain strategy.** Subdomain (`tracker.semurphy.com`) vs subpath (`/tracker`). 3. **Auth between phone and Worker.** Single-user shared secret vs OAuth-with-GitHub vs nothing-but-obscurity. 4. **CI rebuild filtering.** Whether session-only commits should skip the tracker SPA rebuild. 5. **Structured progression rules in TOML.** Whether to encode `progression = { kind, increment, cadence, deload }` now @@ -102,9 +102,9 @@ without bundling them into a SPA build. adds one). - Verify program.json is fetchable from a deployed page. -**Deliverable:** A live URL like `shanemurphy.space/program.json` that returns the compiled program. +**Deliverable:** A live URL like `semurphy.com/program.json` that returns the compiled program. -**Ready when:** `curl https://shanemurphy.space/program.json` returns valid JSON matching the schema agreed in Phase 0. +**Ready when:** `curl https://semurphy.com/program.json` returns valid JSON matching the schema agreed in Phase 0. --- diff --git a/flake.nix b/flake.nix index 47dca0e..b7be6c9 100644 --- a/flake.nix +++ b/flake.nix @@ -1,13 +1,19 @@ { - description = "shanemurphy.space — Zola static site"; + description = "semurphy.com — Zola static site"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; - outputs = { self, nixpkgs }: + outputs = + { self, nixpkgs }: let - systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); in { @@ -31,29 +37,12 @@ devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = [ pkgs.zola ]; - # `nix develop` drops into a bash subshell. When launched from Warp, - # emit Warp's bootstrap escape sequence so the subshell gets blocks, - # completions, etc. - # - # Two guards: - # - $- contains `i` only for an interactive shell. Under direnv the - # hook runs non-interactively inside the already-warpified host - # shell, so there's no subshell to bootstrap — skip it there. - # - $TERM_PROGRAM makes it a no-op outside Warp. - shellHook = '' - case $- in - *i*) - if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then - printf '\eP$f{"hook": "SourcedRcFileForWarp", "value": { "shell": "bash" }}\x9c' - fi - ;; - esac - ''; }; }); # `nix run` / `nix run .#serve` → live-reloading dev server. - apps = forAllSystems (pkgs: + apps = forAllSystems ( + pkgs: let serve = pkgs.writeShellScriptBin "serve" '' exec ${pkgs.zola}/bin/zola --config zola.toml serve "$@" @@ -65,6 +54,7 @@ type = "app"; program = "${serve}/bin/serve"; }; - }); + } + ); }; } diff --git a/sass/_palette.scss b/sass/_palette.scss index d7c09ac..1a4d5a2 100644 --- a/sass/_palette.scss +++ b/sass/_palette.scss @@ -1,4 +1,4 @@ -// ─── Birch & Cream — raw stops ─────────────────────────────────────────────── +// ─── Birch & Cream — raw stops (light) ─────────────────────────────────────── $parchment : #F7F2E8; // lightest bg $raw-linen : #E6DBCB; // surface / footer bg @@ -7,7 +7,31 @@ $caramel : #9A7A5E; // accent text, labels, links $bark : #634D3A; // body / supporting text $espresso : #3A2B1E; // headings, strong text -// ─── Semantic CSS custom properties ────────────────────────────────────────── +// ─── Slate — raw stops (dark) ──────────────────────────────────────────────── + +$slate-bg : #14171A; // lightest bg +$slate-surface : #1C2024; // surface / footer bg +$slate-border : #2C3238; // borders, dividers +$slate-muted : #8A95A0; // accent text, labels, links +$slate-body : #C2CAD2; // body / supporting text +$slate-heading : #F0F3F6; // headings, strong text + +// ─── Semantic tokens ───────────────────────────────────────────────────────── +// +// Every component reads from --color-*. A theme is just a re-binding of these +// six variables. Resolution order: +// 1. Explicit choice via [data-theme] (set by the toggle in base.html). +// 2. OS preference via prefers-color-scheme, when no explicit choice is made. +// 3. Light (the :root default) otherwise. + +@mixin slate-tokens { + --color-bg : #{$slate-bg}; + --color-surface : #{$slate-surface}; + --color-border : #{$slate-border}; + --color-muted : #{$slate-muted}; + --color-body : #{$slate-body}; + --color-heading : #{$slate-heading}; +} :root { --color-bg : #{$parchment}; @@ -16,4 +40,21 @@ $espresso : #3A2B1E; // headings, strong text --color-muted : #{$caramel}; --color-body : #{$bark}; --color-heading : #{$espresso}; + + color-scheme: light; +} + +// Explicit dark choice wins everywhere. +[data-theme="dark"] { + @include slate-tokens; + color-scheme: dark; +} + +// System preference — applied only when the visitor hasn't explicitly chosen. +// An explicit [data-theme="light"] opts back out. +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + @include slate-tokens; + color-scheme: dark; + } } diff --git a/sass/main.scss b/sass/main.scss index 1e77afb..33433ca 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -15,6 +15,7 @@ body { min-height : 100dvh; display : flex; flex-direction: column; + transition : background-color 0.2s ease, color 0.2s ease; } main { @@ -69,8 +70,9 @@ a { } .nav-links { - display: flex; - gap : 1.5rem; + display : flex; + align-items: center; + gap : 1.5rem; a { font-family : sans-serif; @@ -80,6 +82,26 @@ a { } } +.theme-toggle { + font-family : sans-serif; + font-size : 11px; + letter-spacing: 0.06em; + text-transform: uppercase; + color : var(--color-muted); + background : transparent; + border : 1px solid var(--color-border); + border-radius : 20px; + padding : 4px 11px; + cursor : pointer; + min-width : 5.5em; + transition : color 0.15s ease, border-color 0.15s ease; + + &:hover { + color : var(--color-heading); + border-color: var(--color-muted); + } +} + // ─── Hero ───────────────────────────────────────────────────────────────────── .hero { diff --git a/templates/base.html b/templates/base.html index 29a0aba..72057d5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,6 +6,23 @@ {% block title %}{{ config.title }}{% endblock title %} + {# Syntax-highlight palettes (Zola class-based). The media attributes are the + no-JS fallback: the OS preference picks one. The theme script overrides them + to honor an explicit toggle choice. #} + + + {# Apply an explicit choice before first paint to avoid a flash of the wrong + palette. With no stored choice, CSS prefers-color-scheme handles it. #} + {% block extra_head %}{% endblock extra_head %} @@ -17,6 +34,7 @@ Work Writing Fitness + @@ -42,5 +60,43 @@ + {# Light/Dark toggle. No stored choice ⇒ follow the OS via prefers-color-scheme. #} + + diff --git a/zola.toml b/zola.toml index 0c69e06..55006c0 100644 --- a/zola.toml +++ b/zola.toml @@ -5,7 +5,9 @@ compile_sass = true minify_html = false [markdown.highlighting] -theme = "catppuccin-latte" +style = "class" +light_theme = "catppuccin-latte" +dark_theme = "catppuccin-mocha" [extra] hero_headline = "Engineer,
musician,
human." @@ -14,7 +16,7 @@ hero_tags = ["Software", "Space", "Music", "Gaming"] github = "https://github.com/ShaneEverittM" linkedin = "https://www.linkedin.com/in/shane-everitt-murphy/" -email = "mail@shanemurphy.space" +email = "mail@semurphy.com" [[extra.about_facts]] key = "Location"