Skip to main content
❯_
❯main
  • Home
  • About
  • Hello
  • Feed
  • Email
  • Guestbook
❯explore
  • Blog
  • Notebook
  • Now
  • Verify
  • Resume
  • Interests
  • Tags
  • Why
  • Changelog
  • Humans
  • Palette
  • Style
  • Privacy
  • Licensing
  • Projects
  • Colophon
  • Meta
  • Search
  • 8biticon
❯meta
  • PGP Key
  • GitHub
  • LinkedIn
  • Resume.pdf
  • Discord
❯options

Stops the site's blinking cursor flourishes.

Follows your OS/browser preferred color scheme. Manually switching the theme turns this off.

Shows an animated star field behind the site.

Hiragana rain falls behind the site. Enabling this turns off Stars.

Lights a Doom-fire effect on the 404 page. Only visible when you actually hit a missing page.

❯theme

 

All systems nominalEmail copied!
$HOME / asce1062 / changelog_

Alex.


This is a changelog page1.

$git log --oneline_

My Tinkering Log

491 total commits

I know I should stop "playing" with my site all the time and focus on writing; but I am having so much fun learning and experimenting with Astro. Want to see what I've been messing with on the webbed site, instead of actually writing content? Here's a list of changes I've made to my Astro site:

View all commits on GitHub

2026

fix(a11y): make the sidebar visit line and terminal hint readable

October 26, 2026asce1062View on GitHuba47d1e3
Commit details
The nav brand's "visit N · ..." line was base text at 0.18 opacity
(about 1.4:1 in both themes) and the ":open terminal" hint a 58% mix at
0.72 opacity (2.2:1 light). axe skipped the visit line as aria-hidden,
but sighted visitors still read it.

Both now use the muted token, which is AA in every theme, and the idle
state quiets the greeting to muted instead of fading the whole block
below AA. Every text piece in the block measures at least 5.8:1 in
light and dark, active and idle; the lines still sit visibly below the
greeting.

fix(a11y): give every page its own h1, keep the greeting as chrome

October 26, 2026asce1062View on GitHub1488d35
Commit details
55 of 93 pages had no h1. The header's "Alex." greeting is shared site
chrome, so it stays a <p> on every page (reverting the home-page
greetingAsH1 of 64483fdb) and each page now names its own topic:

  • Pages that opened with a title h2 promote it to h1 with no visual

change: the "This is a ... page" slashpages, palette, privacy and SPS
pages use .content-title (the article title step); notes, tag index,
projects, meta, resume and change-password use the global h1.
  • Home and the blog index get screen-reader h1s ("Alex Mbugua",

"Blog"), as the 404 page already does; their design is unchanged.
  • Tag pages promote "found N items tagged with X" to h1, styled as the

body paragraph it was.

The global h1 now takes the title step (20px/24px, heading colour), so
the greeting (24px/36px) is the largest text on every page at every
width. The search, offline, 8biticon, success, /style and admin titles,
which matched or exceeded it, now use the title size.

Verified by diffing computed styles and boxes of every page title at
1280px and 390px: promoted titles changed tag only. A new site check
(npm run check:site, renamed from check:glued) asserts one h1 per built
page and that it stays smaller than the greeting.

fix(links): stop the link check flaking on a busy machine

October 26, 2026asce1062View on GitHubd77d6a0
Commit details
linkinator's default 100 parallel requests made its local server drop
connections under load: a pre-push run reported four links on one post
as broken (status 0, no response) while the files existed and reruns
passed. The check now uses 20 requests at a time and retries network
errors and 5xx. A 404 is never retried, so a planted broken link still
fails at once.

fix(links): render links inline so punctuation stays with them

October 26, 2026asce1062View on GitHub6f52f23
Commit details
Every link was inline-block, an atomic box: the line could break
between a link and its trailing punctuation (the home page ended with
"guestbook" and a lone "." on the next line), and a long link could not
wrap inside itself, leaving ragged gaps on phones (/privacy started
lines with ", signed with" and ". The public key").

Links are now inline, the CSS default. Links that need a box
(link-action, nav links, cards) already set their own display, and no
link that relied on inline-block wraps block content. Checked on 19
pages at 1280px and 390px: page heights unchanged, breadcrumbs and
footnote refs pixel-identical. The 0.5px press nudge now applies only
to links with a box display.

fix(a11y): make the site greeting the home page h1

October 26, 2026asce1062View on GitHub64483fd
Commit details
The home page had no h1, so screen reader users jumping by heading
found nothing to land on. Layout takes greetingAsH1 and the header
renders the "Alex." greeting as the h1 where it is set; everywhere else
it stays a <p>, so content pages keep their own title as the only h1.

Only the home page opts in. The greeting is styled by its class, so the
h1 is pixel-identical to the <p> (same box and computed styles), and
the text effects keep its accessible name as "Alex.".

fix(a11y): bring every theme and flavor to WCAG AA contrast

October 26, 2026asce1062View on GitHuba3d9203
Commit details
Lighthouse audits only the default dark theme; axe across both themes
and all flavors found text below 4.5:1.

  • Light warning (labels, dates, table headers) was 1.7-3.5:1 in the

default theme and every light flavor. Each now sits dark enough for
4.6:1 on its flavor's base-300, keeping hue, with chroma capped to
stay in sRGB. Default light warning-content flips to light, like the
other light status colours.
  • Light primary (links) in default, amber, synthwave, dos, ice and

redline: same rule; card links were 4.35:1.
  • Fill/content pairs: every dark flavor inherited a light

error-content that failed on its brighter error (btn-error,
badge-error), as did redline's primary and accent content. These now
use dark content; light amber secondary and synthwave accent darken
slightly instead.
  • void and redline dark secondary: code keyword highlights were 3.2:1

on the fixed code-block background.
  • ASCII widget: the font label uses the muted token instead of opacity,

and the dice and copy buttons get 24px hit areas (WCAG 2.5.8) with
their centres 24px apart.

The axe sweep now covers flavors in both themes and the blog index,
and reports every failing page. Decorative ASCII art (.pre-art) is
excluded from contrast as WCAG 1.4.3 allows.

chore(lint): lint the Playwright specs

October 26, 2026asce1062View on GitHub70005e2
Commit details
e2e/ matched no ESLint config block, so its specs were skipped. They
run in Node, so they join the block that lints db/ and scripts/.

fix(husky): fail hooks on any step and gate pushes on the new checks

October 26, 2026asce1062View on GitHubf93d2d4
Commit details
prepare ran husky inside asce1062/, where there is no .git, so it
failed silently: fresh clones got no hooks, and the hand-set
core.hooksPath ran hook files bare, without sh -e. Only the last
command's exit code counted, so a lint-staged failure on a commit with
no source files still committed. prepare now installs from the git
root, and every hook sets -e itself.

  • commit-msg: Conventional Commit subject (merge, revert and autosquash

subjects pass), no em dash, no Co-Authored-By trailer.
  • pre-commit: tests also run for staged .mdx and .json, which feed

content and data tests.
  • pre-push: knip and the coverage floors join the type check.

Build-backed checks are opt-in and share one build:
SITE=1 (glued words, links), E2E=1 (journeys, axe),
LIGHTHOUSE=1, or VERIFY=1 for all three.

test: add coverage floors, built-site checks and an e2e suite

October 26, 2026asce1062View on GitHubeb805f1
Commit details
Coverage (npm run test:coverage): v8 coverage with floors just under
today's numbers, global and stricter ones for src/lib/api, db and
textEffects. A drop fails the run.

Built-site checks, run against dist/ after a build:
  • check:glued finds words that render glued across an inline tag

(read<em>more</em>). It runs in Chrome so layout decides what shares
a line: flex and grid children, block, hidden and out-of-flow
elements never glue.
  • check:links crawls dist/ with linkinator, including #fragments.

Exact-path Netlify redirects are rewritten to their targets, which
must exist; prerender = false routes are skipped. Both lists come
from netlify.toml and src/pages, so they cannot go stale. External
links are skipped.

E2E (npm run test:e2e): Playwright journeys (sidebar navigation, 404,
theme toggle and flavor swatch with persistence, music player dialog)
and axe WCAG 2.2 A/AA checks on each layout in both themes, plus a
contrast sweep over every flavor.

Playwright serves dist/ with serve and drives the installed Chrome:
it no longer ships Chromium for macOS 13. SSR routes are not in dist/,
so nothing here reaches the production database or email.

fix(content): stop words gluing across tags and BEM names

October 26, 2026asce1062View on GitHub71f362b
Commit details
Post tags were separated by margin only, so their text ran together as
"securityauthenticationargon2" in copy-paste, reader mode and search
excerpts. A real space now separates them; it replaces the 8px margin,
so the visual gap stays about the same.

The changelog renderer bolded text even inside a word, turning
commit messages that mention terminal-window__body into bold runs.
Underscores now bold only at word edges, as in CommonMark.

refactor(theme): remove the unfinished transition motifs

October 26, 2026asce1062View on GitHubb40c53c
Commit details
The motif engine (scanline, phosphor, retune, glitch, ripple, static,
terminal) shipped without its overlay element or picker UI, so no motif
ever ran. Its registry, manager and state modules, the unused
switchFlavor(), the overlay keyframes and the picker pill CSS were dead
weight that knip reported as unused exports.

Flavors are untouched: the nine palettes, swatch picker, setFlavor()
and the flavor-change event all stay. atomicSwitch() keeps only the
transition freeze that stops the stagger wave on theme and flavor
switches, now covered by a DOM test.

The theme-transition storage key is gone too, so the privacy page no
longer lists it. The work is kept on the local parked/theme-transitions
branch.

chore(lighthouse): drop the Netlify Lighthouse plugin

October 25, 2026asce1062View on GitHub07f2aab
Commit details
@netlify/plugin-lighthouse reported a failed run on every deploy
(a11y 0.98, SEO 0.95-0.99, one perf 0.8) while current Lighthouse
scores the same deploy at a11y 100 and well inside the targets. Its
latest release, 6.0.4, still bundles Lighthouse 9.6 (2022):

  • Its axe-core cannot parse oklch(), which the whole theme uses

("Unable to parse color oklch(0.15 0.014 65)"), so the a11y score
is not a real measurement.
  • Its SEO score includes tap-targets, the retired 48px rule. It flags

the 26px mobile header terminal trigger, which meets WCAG 2.5.8's
24px and passes Lighthouse 12's target-size. Lighthouse 9.6.8 run
locally reproduces the plugin's SEO 0.95 and 0.99 exactly.

There is no newer release to upgrade to, so the plugin goes.
npm run lighthouse (Lighthouse CI, current Lighthouse, same targets)
stays the check.

The README's plugin list is corrected too: none of the four plugins it
named are configured.

perf(music): stop the closed player's cover art competing with LCP

October 25, 2026asce1062View on GitHubb5bc1ee
Commit details
The music player's markup is on every page and starts closed, but its
cover <img> was eager, and browsers fetch eager images even under
display: none. On the blog post's mobile Lighthouse run the 52 KiB
album-art.png was requested in the same burst as the 66 KiB LCP hero
and shared its bandwidth on slow 4G (hero load time ~830 ms, LCP
2.47-2.69 s against a 2.5 s target).

The cover is now loading="lazy", so it is fetched when the player is
first shown. Verified on the built site at 412px and 1280px: no request
for it on page load or after scrolling, and opening the player loads
and renders the cover at 144x144.

fix(a11y): give the sidebar collapse tab a 24px hit area

October 25, 2026asce1062View on GitHube9ba540
Commit details
The bookmark tab tucks 14px of its 24px behind the sidebar, so only a
~10-15px strip was clickable and Lighthouse's desktop target-size audit
failed (a11y 96 on home).

The button box is now 14px wider. That extra space is transparent padding
over the empty gutter beside the sidebar, so the exposed clickable part
is 24px. The visible tab (background, border, glow, focus ring) moved to
a ::before sized to the original 24px, and the padding also accounts for
the tab's right border so the icon stays where it was.

Verified in the built site at 1280px, expanded and collapsed: axe
target-size passes, a click 20px right of the sidebar edge toggles the
sidebar, and the resting tab matches production at 3x.

Also corrects the block comment, which still described the clip-path
the tab no longer uses.

perf(images): serve the LCP images as AVIF at the size phones need

October 25, 2026asce1062View on GitHubb2c7c83
Commit details
On production the blog post's hero and the blog index's first card were
already eager and fetchpriority=high, so LCP was the image download
itself: a phone (412px, DPR 1.75 needs ~650px) took the 816w/960w WebP,
~145 KiB, ~2.2 s of the post's 3.3 s LCP on slow 4G.

  • PreviewImage and ContentCard render <Picture> with AVIF first and

WebP as the fallback. The picture element is display: contents, so the
img keeps its existing flex layout.
  • A 720w step was added to both srcsets, and sizes now matches the

measured box (content width = 100vw - 40px on mobile).

For the post's hero on that phone: 720w AVIF is 68 KiB against the
816w WebP's 148 KiB (-54%); a side-by-side crop at the same size shows
no visible difference, including the small in-image text. Layout is
unchanged: the hero is still 372x288 at 412px and 816x288 at 1280px,
and Chrome picks the AVIF candidates.

chore(lighthouse): add a local Lighthouse check and align the deploy audit

October 25, 2026asce1062View on GitHubd81aba5
Commit details
Targets for a personal site, not a 100 chase: performance >= 0.9 with
Core Web Vitals in Google's "good" range (LCP <= 2.5 s, TBT <= 200 ms,
CLS <= 0.1), accessibility 100, best practices >= 0.95, SEO 100.

  • npm run lighthouse builds the site and runs Lighthouse CI

(lighthouserc.cjs) on the static output: home, blog index and a post,
mobile profile, median of 3 runs, asserting the targets. Reports go
to .lighthouseci/ (gitignored) and are never uploaded.
  • The Netlify Lighthouse plugin already ran on every deploy, on / only,

with 0.9 thresholds that never failed a deploy. It now audits the same
three pages against the same targets. It still only reports
(fail_deploy_on_score_thresholds = "false") until the targets hold on
real deploys; SEO 100 applies to production since previews are
noindex.

Verified: lhci autorun runs end to end and asserts; on a heavily
loaded machine accessibility, best practices and SEO were 100 on all
three pages while performance (86 / 76 / 63) is still below target.

fix(perf): harden the on-demand loaders

October 25, 2026asce1062View on GitHub92bf242
Commit details
Review findings on the lazy-loading changes:

  • Sidebar egg: its dice and copy buttons were focusable (and announced)

before the art had loaded, and did nothing. The egg now ships inert
and sidebarEgg clears inert once the widget is set up.
  • Floating search: if the search UI failed to download, the modal

opened empty. It now says "Search could not load. Check your
connection and try again."; the next open retries and clears the
message before building.
  • Terminal and music player: repeated triggers while the code was still

downloading each re-dispatched the open event. Only the first one
does now.
  • Page cache: NetworkFirst had no timeout, so on a stalled connection a

visited page waited for the browser's own timeout instead of using
its cached copy. networkTimeoutSeconds: 3.

Verified in Chrome: the egg is inert until its art is drawn and usable
after; with the search chunk blocked the modal shows the message, and
once unblocked the next open builds search with the input focused and
no stale text; a double Ctrl+Alt+T on a fresh page opens the terminal; the built
service worker carries networkTimeoutSeconds:3.

perf(music): load the music player on first use

October 25, 2026asce1062View on GitHub1f42cb2
Commit details
The music player's script (~32 KiB) was a static import on every page
and booted at load (grid, flavor sync, listeners) for a window that
starts hidden and that most visits never open.

The component script now only watches for the first activation of a
[data-music-player-trigger] (click, or Enter/Space): it imports the
player, which boots itself because the page is already loaded, then
dispatches the "music-player:open" event the player already listens
for. From then on the player handles its triggers and astro:page-load
itself, exactly as before. Pointer-over or focus on a trigger starts
the download early. Its window, overlay and dock chip are hidden in
the markup, so nothing changes before it opens.

Verified in Chrome: no player code loads with the page; focusing the
homepage trigger prefetches it; Enter, the homepage icon and the
sidebar Music button open it with the visualizer built; minimize docks
it, the dock survives a soft navigation and restores the window; no
console errors besides the music API being absent on a static server.

perf(ascii): print the console greeting once the page has settled

October 25, 2026asce1062View on GitHubb580e63
Commit details
The console easter egg printed on astro:page-load, which now fetches the
greeting art on demand. Schedule it with requestIdleCallback (4 s
timeout; 2 s timer fallback) so that fetch never competes with the
page's own loading. The console is never urgent.

perf(terminal): load the terminal on first open

October 25, 2026asce1062View on GitHubde54221
Commit details
The terminal modal's script (~57 KiB, plus the ~66 KiB ASCII art its
neofetch command imports) was a static import on every page, though the
window starts hidden and most visits never open it.

Every way of opening it (the sidebar's terminal triggers and Ctrl+Alt+T)
already dispatches NAVBRAND_OPEN_TERMINAL_EVENT, so the
component script now only listens for that event: on first use it
imports the terminal, initializes it for the current page and
re-dispatches the event; from then on the module handles the event and
its own astro:page-load re-init exactly as before. Hovering or focusing
a terminal trigger starts the download early. The window, overlay and
dock chip are hidden in the markup, so nothing changes before it opens.

Verified in Chrome: no terminal code loads with the page; Ctrl+Alt+T
and the sidebar prompt open it with the input focused; commands run;
minimize docks it, the dock survives a soft navigation and restores the
window with its log; no console errors.

perf(ascii): load the greeting art only when it is shown

October 25, 2026asce1062View on GitHub400b6f0
Commit details
The figlet greeting variants (src/data/ascii-art.json, ~66 KiB) went out
twice on every page: bundled into the console easter egg's script, and
inlined into the HTML as the sidebar egg's data-variants attribute
(60 KiB raw, 7.8 KiB gzipped: about 30% of the homepage HTML). The
sidebar egg then parsed and drew its art at page load (~130 ms under 4x
CPU throttle), though it sits at the bottom of the sidebar, and inside
the closed drawer on mobile.

  • src/scripts/asciiGreetings.ts loads the variants once, on demand, for

both eggs (one shared chunk).
  • The console egg awaits it before printing.

  • The sidebar no longer inlines the variants; sidebarEgg sets up the

widget when the egg comes within 200px of the viewport, so the reveal
animation still plays when it scrolls into view.
  • setupAsciiWidget accepts variants from the caller; about and 404 keep

their inline variants.

Homepage HTML: 25.5 KiB -> 16.5 KiB gzipped. Verified in Chrome: no art
is fetched or drawn until the sidebar is scrolled to the egg, which then
reveals and re-rolls with the dice (updating the brand name); the
console greeting still prints; the about page widget is unchanged.

perf(search): load the Pagefind UI only when search is opened

October 25, 2026asce1062View on GitHubf546573
Commit details
Every page statically imported the Pagefind UI (~94 KiB of JS) for the
floating search modal and built it at idle, and its styles (~24 KiB raw)
were part of the render-blocking site stylesheet, though most visits
never open search.

  • src/scripts/pagefindSearch.ts owns search setup and loads the UI with

a dynamic import. The /search page builds its box on load as before;
the floating modal builds its box on first open and focuses the input
once it exists (not if the modal was closed meanwhile). Hovering or
focusing the search button starts the download early.
  • src/scripts/pagefindUi.ts is the lazy chunk: the UI plus its styles as

inlined strings, injected as one <style> when a box is built. Astro
links every stylesheet reachable from a page script into <head>,
dynamic imports included, so plain CSS imports would still load on
every page. Injection is re-checked per build because soft navigation
replaces <head>.
  • One build per box even when opened twice while loading; a failed

download resolves null and the next open retries.

Verified in Chrome: the home page loads only the 3 KiB loader; Ctrl+K
and the button open search with the input focused and results
returned; Escape restores focus; after a soft navigation the styles are
re-added on open; /search still syncs ?q=. The open modal is
pixel-identical to before. New jsdom tests cover the single build under
concurrent opens, retry after a failed load, and ?q= only on /search.

perf(fonts): preload only the body font

October 25, 2026asce1062View on GitHub36292c2
Commit details
Every page preloaded 0xProto Regular, 0xProto Bold and the icomoon icon
font (76 KiB woff) at high priority, ahead of the stylesheet. On a slow
mobile connection that is ~180 KiB competing with the render-blocking
CSS before the first paint, while the main text (the LCP element) only
needs the Regular weight.

Preload Regular only. Bold, italic and the icons still load from their
@font-face rules as soon as text that uses them is styled, with
font-display: swap as before.

fix(a11y): name and expose the sidebar, share and guestbook controls

October 25, 2026asce1062View on GitHub0fc55d9
Commit details
Lighthouse/axe failures from the mobile and desktop audits:

  • Share control on posts was an <a> with no href, so it was not a link,

was not keyboard-activatable, and its aria-label was prohibited on a
role-less element. It is now a <button> (Enter and Space work), styled
to match the neighbouring share links (link color, primary hover).
  • Guestbook "randomize" button had no accessible name: it is now

"Randomize entry style" and its icon is aria-hidden.
  • Sidebar option "More info" hints put aria-label on a <sup> with no

role. They are a hover-only cue and each toggle is already described
by the same helper text, so the hint is aria-hidden.
  • Sidebar ASCII easter egg was aria-hidden while holding the focusable

dice and copy buttons, so keyboard users landed on controls screen
readers were told do not exist. The container is no longer hidden;
AsciiWidget already hides the art itself.

fix(a11y): code highlighting meets AA contrast in both themes

October 25, 2026asce1062View on GitHub12f84aa
Commit details
Lighthouse flagged Rose Pine's keyword color in dark mode (3.38:1 on
the code background). Checking every token color showed 2 of 9 below
4.5:1 in dark mode (Rose Pine) and 7 of 9 in light mode (Rose Pine
Dawn, low-contrast by design: its gold was 2.05:1). Lighthouse only
audits the dark theme, so light-mode readers were the worst off.

The themes are now loaded as objects with each failing token color
replaced by the same OKLCH hue and chroma at the nearest lightness that
reaches ~4.6:1, so code still reads as Rose Pine. Only token colors
change; the editor UI colors and theme names (used by the data-theme
selectors) stay as they are.

A test asserts every token color of both themes meets 4.5:1 on the
theme's own code background, so a Shiki theme update cannot regress it.

fix(a11y): accessible names include the visible label text

October 25, 2026asce1062View on GitHubd75ff1d
Commit details
WCAG 2.5.3 (Label in Name): a control's accessible name must contain its
visible text, or a voice-control user who says what they see ("click
Changelog") gets nothing. Lighthouse flagged the sidebar on every page
and the homepage music link; a scan of the built site found the same
pattern elsewhere.

  • Sidebar nav links: drop the descriptive aria-labels ("Project release

history and recent changes" for "Changelog") so the visible name is
the accessible name. The collapsed sidebar hides the text with
max-width/opacity, so it stays in the accessibility tree. The
ariaLabel navigation field is now unused and removed.
  • Terminal trigger: the decorative prompt chrome (user@host, box

drawing, visit counter) is aria-hidden and the "Open terminal" label
is dropped; the name is the visible greeting plus "open terminal".
  • Homepage "chip musician", the success page guestbook link and two

in-post links: drop the mismatched aria-labels.
  • Project cards: "view source of <repo> on GitHub".

  • Terminal dock chip: "restore signal: reopen the terminal".

  • Music theme button: the label starts with the visible flavor and is

updated whenever the flavor changes.

fix(music): show a readable error when a playlist request fails

October 25, 2026asce1062View on GitHuba94eca9
Commit details
fetchJson passed payload.error straight to new Error(). The music API
returns a string there, but error bodies from anything in front of it
(a static server, a proxy, a platform error page) can carry an object,
and the playlists panel then read "[object Object]".

Use the existing text() helper so only a non-empty string message is
shown, falling back to "Request failed: <status>".

Verified in the browser: with the API unavailable, the panel now reads
"Request failed: 404".

perf(music): mount the hidden visualizer without a long task

October 25, 2026asce1062View on GitHub79aa4ef
Commit details
The music player boots on every page load, closed. Its ASCII visualizer
mount appended 12 rows of 32 spans straight into the live page and then
started the idle animation, which the caller stopped again on the next
line because the player was hidden. Under Lighthouse's 4x CPU throttle
the mount cost ~360 ms on every page, most of it style invalidation
from the live appends.

  • Build the grid in a DocumentFragment and insert it once.

  • mount() starts whichever loop the current visibility calls for, and

the player sets visibility before mounting, so a closed player starts
no loop at all.

Mount now costs ~2 ms at 4x throttle. Opening the player still renders
the 12x32 grid and animates. New jsdom tests cover the single
insertion, no loop while hidden, the idle loop when visible, and the
reduced-motion static frame; the first two fail on the old code.

perf(images): load the above-the-fold image first and at its display size

October 25, 2026asce1062View on GitHub5924ffb
Commit details
Lighthouse named the post hero (PreviewImage) as the LCP element on blog
posts and the guestbook, yet it was loading="lazy": the browser holds
lazy images back until layout, so the page's main image started late. It
also always served the 1254px original (296 KiB) into a box at most
816px wide, 372px on a phone.

  • PreviewImage is always the first thing under the title, so it loads

eagerly with fetchpriority="high" and a 480/816/1632w srcset sized to
its 816px maximum (a phone now takes the 54 KiB 480w file).
  • ContentCard takes a priority prop; the blog index and tag pages set it

on their first card, which is above the fold on mobile. Other cards
stay lazy. Card images get a 288/480/960w srcset for their 144px
desktop / full-width mobile size.

perf(pwa): precache the app shell, not 99 MiB of pages and images

October 25, 2026asce1062View on GitHub87ed658
Commit details
The service worker precached every built png/jpg/webp/html file: 440
entries, 98.7 MiB, downloaded in the background on a visitor's first
page view. 74 MiB were Astro's original full-size images, which no page
references (pages use the optimized webp; the originals exist for the
SSR image endpoint), 16 MiB were the manifest install screenshots, and
14 MiB were every HTML page including all tag pages. On mobile that is
real data cost, and it kept the network busy long after load (TTI).

  • Precache the app shell only: bundled JS/CSS, the web fonts, favicons,

and the home, about, hello, now and offline pages. 63 entries, 1.8 MiB.
  • Every other page is cached network-first as it is read, so visited

pages work offline and unvisited ones show the offline page. Only
plain 200s are stored, so redirects are never replayed from the cache.
  • Admin, guestbook and search navigations stay network-only, never

cached.
  • Images keep their existing runtime caches.

  • The icomoon stylesheet is now runtime-cached with the fonts; offline

pages previously lost every icon because it was never cached.
  • The /api/ rule was a regex tested against the full URL, so it never

matched; it now matches on the pathname.

Verified in Chrome against the built site: 62 precached entries on
install; /blog/ and /colophon/ cached once visited (not the /colophon
redirect itself); offline, visited and precached pages render with
fonts and icons, an unvisited page shows the offline page, and no
console errors.

perf(fonts): subset 0xProto from ~1 MB to ~50 KiB per weight

October 25, 2026asce1062View on GitHub870e586
Commit details
0xProto ships as the Nerd Font Mono build: 11,161 code points, 10,396 of
them Nerd Font icons in the Private Use Areas, which made each weight
~1 MB as woff2. All three weights were preloaded at high priority, so
on Lighthouse's throttled mobile profile the page text swapped fonts
only after ~3 MB arrived: LCP 17.9 s on the homepage (95% "render
delay"), and the re-layout with an 11k-glyph font was the 1.3 s long
task behind most of the TBT. The site renders no Nerd Font icons.

  • scripts/subset-fonts.mjs (npm run fonts:subset) splits each weight

into the text glyphs (50-58 KiB, all GSUB/GPOS features kept) and one
icons file (884 KiB). The icons @font-face declares the PUA
unicode-range, so a browser only fetches it for a page that renders an
icon glyph; nothing is lost.
  • The full TTFs move to src/assets/fonts/0xProto (not served); the

Open Graph renderer reads them from there. The ttf fallbacks are
dropped from @font-face: every supported browser takes woff2.
  • Italic is no longer preloaded.

  • The OFL license is kept beside the served subsets, as the license

requires for modified versions.
  • The service worker no longer precaches legacy ttf/eot/svg font files

or the icons file.

Verified: glyph coverage of each subset equals the source minus the
PUA (0 missing), the icons file holds exactly the 10,396 PUA glyphs,
OG images build, and the homepage renders 0xProto from the ~51 KiB
files with the icons file never requested.

feat(admin): logout revokes every admin session server-side

October 25, 2026asce1062View on GitHube475852
Commit details
Admin sessions are HMAC-signed cookies; Argon2 verifies the raw token
only at login. Logout used to delete the cookie in that one browser, so
a copied cookie stayed valid for up to 7 days and the only way to kill
it was rotating ADMIN_TOKEN_HASH (review item P1-11).

A single-row AdminSession table (migration 0003) holds a generation
number. Session cookies (now v2) are signed with the generation current
at login and are accepted only while it is unchanged. Logout increments
it, which revokes every outstanding session at once: with one moderator,
single log-out is sign-out-everywhere.

  • Format, expiry and signature are checked before any I/O, so forged or

expired cookies never reach the database; a genuine one costs one
small read.
  • The cookie signing key is derived from ADMIN_TOKEN_HASH with HKDF

instead of using the hash itself as the key. Rotating the hash still
revokes everything.
  • Fails closed: if the generation cannot be read the request is not

authenticated and login reports "Could not start a session".
  • Only a genuine session may revoke, so an anonymous logout POST (Origin

headers are spoofable outside a browser) cannot sign the moderator
out. A genuine session whose generation read failed still tries, and
a failed revocation is reported instead of shown as a clean logout.
  • v1 cookies are rejected: one fresh login after deploy.


Deploy order: apply migration 0003 (npm run db:setup) before this code
goes live, or admin login fails closed until it is applied.

Verified: unit tests for every cookie edge case and the check order, a
real-libsql test for revoke/re-login/missing row, and an end-to-end run
on the isolated dev server: two browsers signed in, logout in one
signed out both, the copied cookie was rejected, an anonymous logout
POST revoked nothing, and logging in again worked.

fix(content): keep long footnote links inside the layout width

October 25, 2026asce1062View on GitHub0d81785
Commit details
The footnotes section sits after the site footer, outside the article,
so the prose wrap rule never reached it. A long unbroken URL in a
footnote (creativecommons.org/compatiblelicenses on the licensing note)
pushed the page 32px wider than a 390px screen.

The footnotes section now uses overflow-wrap: anywhere, which also
shrinks the min-content width of inline-block links. Footnote links
(not the back-references) flow inline so a wrapped URL keeps its
trailing punctuation on the same line, with box-decoration-break: clone
so the hover highlight covers each wrapped line.

Verified on the built site: all 20 footnote pages at 320, 390, 768 and
1300px have no element past the footnotes column and no horizontal
page overflow. Placement after the footer is unchanged.

Revert "fix(content): render footnotes inside the post, not below the site footer"

October 24, 2026asce1062View on GitHub9f68f47
Commit details
This reverts commit 83fc50b4599646f8fc7867167ea0e6e7d20e98ce.

fix(content): render footnotes inside the post, not below the site footer

October 24, 2026asce1062View on GitHub83fc50b
Commit details
GFM footnotes are appended to the end of the MDX document. The posts
wrap their content in <ContentLayout>, so the footnotes section landed
after the layout as a direct child of <body>: below the site footer and
commit line, outside the article and its prose styles. Global CSS had
compensated with a sidebar offset, a max width and 12vh of bottom
padding. All 20 pages with footnotes were affected.

A rehype plugin (rehypeFootnotesIntoLayout) moves the footnotes section
to the end of the root-level layout element, so it renders at the end
of the post body. The footnote CSS drops the offset workaround and keeps
plain in-article spacing.

Links in prose are inline-block, which sizes to the unbroken word, so a
long URL (creativecommons.org/compatiblelicenses) ran past the column;
they now use overflow-wrap: anywhere.

Verified on the built site: all 20 pages have their footnotes inside
the article, before the footer, aligned with the post body at 390px and
1300px, and the licensing note's long link wraps without overflow.

fix(layout): haunted creatures no longer resize the page

October 24, 2026asce1062View on GitHube5d49b6
Commit details
Haunted creatures fly up to 200px out of their container. Absolutely
positioned, transformed boxes still count toward the scrollable
overflow of the page, so hovering a guestbook card near an edge grew
the document (at 390px wide the page went from 382px to 577px wide; on
desktop the last card lengthened it by 147px) and scrollbars flickered
in for the length of the flight.

The content column (.sidebar-content-offset) is now overflow: clip.
Creatures still drift out of their card across the page background and
are clipped only at the column's edges (viewport edge, sidebar, page
bottom). clip is not a scroll container, so position: sticky children
(header, floating TOC) keep working, and fixed overlays (search,
terminal, music player) are not clipped.

A crawl of every built page at 390px and 1300px found two elements that
overflowed the page and would now be clipped instead; both are fixed
here rather than hidden:
  • palette view tabs overflowed by 4px at 390px: px-3 below sm

  • long unbroken words or URLs in prose wrap (overflow-wrap: break-word

on article paragraphs and list items)

Verified in Chrome: no document growth with creatures in flight at
390px (first card) and 1300px (first and last card), creatures still
visible beyond the card, sticky TOC unchanged, and the crawl reports no
clipped content.

fix(a11y): palette view tabs follow the ARIA tabs pattern

October 24, 2026asce1062View on GitHuba004018
Commit details
The palette views switch with CSS-only radios, with each label marked
role=tab. Keyboard users met four stops for one control (the checked
radio and all three labels), no tab exposed aria-selected, and the
panels were labelled by the unlabelled radios (P13-7).

New paletteTabs.ts upgrades the labels to real tabs: aria-selected and
a roving tabindex mirrored from the checked radio, Arrow keys (wrapping),
Home and End, Enter/Space. The radios still drive the CSS but leave the
Tab order and the a11y tree, and focus that lands on one (a label click
focuses it) moves to its tab. Selection syncs on the radio's change
event, since a label click checks the radio only after click listeners
run. Panels are labelled by the tabs. Without JavaScript the radios
remain the working control, so tab state is set by the script only.

The Enter/Space handler moves out of paletteCopy.ts.

Tests cover the initial state, arrows/Home/End, a label click and the
focus redirect. Verified in Chrome.

fix(a11y): floating search is inert when closed and traps focus when open

October 24, 2026asce1062View on GitHub4be91f1
Commit details
The search dialog is aria-modal, but closed it was only faded out
(opacity-0, pointer-events-none): its input and buttons stayed in the
Tab order and the accessibility tree, so keyboard users tabbed into an
invisible search. Open, Tab walked out into the page behind (P12-5).

  • the modal is inert while closed; open/close toggle it, so the fade

transition is unchanged
  • while open, Tab and Shift+Tab wrap inside the modal, and a Tab from

outside it (e.g. right after Ctrl+K) moves into it; focusables are
read per keypress because Pagefind adds result links

Verified in Chrome: 120 Tabs never reach the closed search; open with
results, Tab and Shift+Tab stay inside; Escape returns focus to the
opener.

fix(a11y): avatar mini widget exposes gender and announces saves

October 24, 2026asce1062View on GitHubd700238
Commit details
The gender buttons showed the active gender with a class only, and the
save feedback was a floppy -> check icon flash (P11-11).

  • gender buttons carry aria-pressed, set with the active class and on

bind
  • each widget has a visually hidden role=status that announces "Avatar

saved" (cleared first so a repeat save re-announces, cleared after
3s; timers stop on abort)

Verified in Chrome: the tree reads "Female avatar [pressed]" and
"status: Avatar saved"; the widget layout is unchanged.

fix(a11y): avatar layer tabs follow the ARIA tabs pattern

October 24, 2026asce1062View on GitHub405d26b
Commit details
The layer tablist had no tabpanel, every tab was a Tab stop and arrow
keys did nothing (P11-10).

  • tabs get ids, aria-controls and a roving tabindex (one Tab stop)

  • the layer grid is a tabpanel labelled by the selected tab

  • Arrow keys (wrapping), Home and End move and select

  • one markActiveTab helper replaces the three copies of the tab sync;

gender resets follow the state manager's current layer (the first
configured layer) instead of a hard-coded index

Also fixes the last tab label ("Background") being clipped: the strip
overflowed its 388px column by 6px, and by 4px at 390px wide. Tab
min-width 3.65rem -> 3.5rem, and no gap at <=420px. Narrower screens
still scroll the strip, as designed.

Verified with the keyboard on /8biticon: arrows, End, wrap, Tab into
the panel, gender switch resets to Face.

fix(a11y): guestbook entry menu returns focus and owns Escape only when open

October 24, 2026asce1062View on GitHub4ec84ed
Commit details
Closing an entry action menu (Escape, or choosing an action) hid it
while focus was inside, dropping keyboard focus to <body>. The document
keydown handler also acted on every Escape on the page, and tabbing out
left the menu open (P11-4).

  • a menu that holds focus hands it back to its trigger when it closes

  • Escape and arrow keys are handled only while a menu is open

  • focus moving out of an open menu closes it without stealing focus


Tests cover Escape, choosing an action, Escape with no menu open, and
tabbing away (three fail on the previous code). Verified with the
keyboard on the guestbook page.

fix(music): a stalled stream no longer locks the player controls

October 24, 2026asce1062View on GitHubb8094b2
Commit details
isStreamLoading disabled play, stop, previous, next, shuffle, repeat and
the progress bar (and made their handlers return early) until play()
settled. On a stalled stream it never settles, so every control stayed
disabled and a focused control dropped keyboard focus (P8-7).

Remove the flag: each playTrack already supersedes the pending one
(playRequestId, and a new src aborts the old load), so next, previous
and stop can interrupt a stalled load.

Resuming (togglePlayback, restartTrack) now goes through resumePlayback,
which applies the same guard as playTrack: a play() rejected because a
newer request superseded it or pause() aborted it is not a failure.
Pausing a stalled resume used to replace "stream paused" with "playback
blocked".

Verified in Chrome with the stream request held open: controls stay
enabled, Next switches track and keeps focus, Stop pauses at 0:00,
pausing a stalled resume shows "stream paused" (the old catch showed
"playback blocked"), and normal playback is unchanged.

fix(a11y): one h1 per post, section headings start at h2

October 24, 2026asce1062View on GitHubae0f91a
Commit details
Eight posts used # for section headings, so they had several h1s
next to the post title (the page's h1), and the resume's Timeline and
Skills section titles were h1 elements.

Each affected post's heading levels are renumbered by rank, so its
first level becomes h2 and the rest follow in order (h1/h3/h4 become
h2/h3/h4, which also closes the skipped level). The TOC plugin nests by
relative level, so every TOC tree and its indentation are unchanged:
the built TOC markup is byte-identical for seven posts. In the eighth
(password security) the generated Footnotes h2 now sits at the top
level beside the lead section instead of under it, as in every other
post with footnotes. Timeline and Skills titles are h2.

Section headings in these posts render one size step smaller, matching
the posts that already started at h2. Also un-indents a closing code
fence in IcoMoon part 3 (four spaces is not a closing fence in
CommonMark).

Verified with a full build: every page has exactly one h1.

fix(a11y): screen readers read settled text, not effect frames

October 24, 2026asce1062View on GitHub6cabb6f
Commit details
Text effects rewrite an element's text many times a second (scrambled
glyphs, cursors, half-typed words). Screen readers read those frames,
the aria-live sidebar greeting and terminal log announced them, and the
looping 404 typewriter heading never settled (P7-5, P9-4).

The engine now splits a host on its first animated playback into an
aria-hidden frame, which effects render into, and a visually hidden
label holding the stable text. The label is set once per transition
(to the destination text) and left alone when unchanged, so live
regions announce real text once. Reduced motion and non-DOM hosts keep
plain text. All settled-text writes and reads go through the surface,
so nothing reads frame + label as doubled text, and the glitch shimmer
(which runs on the frame) is still stopped when its host is cleared.

Consumers:
  • terminal: log entries get their surface before entering the log so

they are announced once; the log is role=log; the presence header is
no longer a second live region; the font tag and ASCII art rows are
aria-hidden
  • terminal atmosphere and music signal label reset before writing the

new text: cancelling a running effect restores that effect's own
target, which reverted the new text
  • AsciiWidget art (404, about, sidebar egg) is aria-hidden

  • the 404 burn skips screen-reader-only text, which still reports glyph

rects and would ink a second copy
  • the label is user-select: none so copying a line pastes it once


Verified in Chrome: the accessibility tree reads clean text while the
frames scramble (404 heading and paragraph, sidebar, terminal log), the
layout is unchanged, the burn follows the visible glyphs and a copied
paragraph has no duplicate. New jsdom tests cover the surface,
transitions, reduced motion, reset, the looping typewriter, content
change, a lost label and the shimmer stop (fails without the
frame-keyed stop).

fix(github): fail the build on GitHub API failures

October 24, 2026asce1062View on GitHubd60066a
Commit details
The two GitHub-backed pages disagreed: a failed repo list failed the
build, but a failed commit fetch shipped an empty changelog (or a
partial one when a later page failed), and failed language/contributor
lookups silently rendered cards with no languages and 0 contributors.

One policy now: GitHub data is complete or the build fails, so Netlify
keeps serving the last good deploy. Every request goes through
githubFetch, which retries transient failures first (network errors,
30s timeouts, 5xx, 429, and 403 secondary rate limits that send
Retry-After; 3 attempts, waits over 60s are not taken) and releases
the response body before retrying. A primary 403 rate limit resets in
up to an hour, so it fails fast. An empty repository's 204 still counts
as 0 contributors.

changelog.astro drops its unreachable error and empty states, and a
SOCIAL.repo that is not a GitHub URL now throws instead of rendering
nothing.

Verified with a production build against the real API (438 commits,
61 project cards) and the built page in Chrome.

fix(db): normalize legacy guestbook timestamps to ISO

October 24, 2026asce1062View on GitHub71c1dbb
Commit details
Rows written by the SQL default (CURRENT_TIMESTAMP) store
"YYYY-MM-DD HH:MM:SS" while the app writes ISO 8601, so production
held two formats in every date column. Migration 0002 rewrites valid
legacy values to the exact toISOString shape (UTC kept) in Guestbook
timestamp/moderatedAt/adminNotifiedAt/copySentAt and the moderation
log's at column.

Only values with the legacy shape that also round-trip through
strftime are touched, so ISO values, NULLs and malformed text are left
alone rather than nulled (a NULL timestamp would fail NOT NULL and
roll the migration back). Re-running is a no-op.

Test covers the SQL default, mixed and malformed values, NULLs and the
log table on a database stopped before 0002.

fix(toc): stop the floating TOC flickering when it is opened

October 24, 2026asce1062View on GitHub3bc3273
Commit details
Opening the floating (stuck) TOC grows it by ~450px and Chrome's scroll
anchoring scrolls the page down by the same amount to hold the reading
position. That scroll was handled before the asynchronous toggle event,
so the open-state flag still read "closed": the jump counted as a
deliberate scroll down and hid the TOC (translateY(-100%)), and the
toggle event ~100ms later showed it again.

Read details.open in the scroll handler instead of a flag updated on
toggle. Reproduced in Chrome (scroll down, scroll up to reveal, open):
style flipped hidden -> visible before; no transform change after, at
1440px and 390px, and scrolling down afterwards still hides it.

Add jsdom tests for the hide-on-scroll-down path and the anchoring jump
on open and close (the two new ones fail on the previous code).

fix(a11y): post title is the page h1, the TOC label an h2

October 24, 2026asce1062View on GitHub51b7002
Commit details
  • ContentLayout renders the post title as the h1 (was h2), so the

document outline starts with what the page is about
  • the table of contents is a <nav> landmark labelled by its heading,

which is now an h2 (was the page's only h1), and is excluded from the
Pagefind index so search results stop matching TOC link text
  • .content-title and .toc-title keep the previous sizes, line heights,

margins and colours: the visual hierarchy is unchanged, verified by
computed styles at 1280px and 390px

Decision P12-8 from the site review.

fix(styles): restore the details callout rule

October 24, 2026asce1062View on GitHubd96c5fd
Commit details
cd36ad3f deleted the dead #expand-content p selector together with the
rule body it shared with details > :nth-child(n + 2). The orphaned
selector then joined the following table rule, so every <details>
outside the TOC (meta, changelog, privacy policy, search, sidebar) lost
its warning left border and padding and rendered its content with the
table styles (block, 100% width, 1.5rem margins, 0.875rem text).

Restore the rule for the details selector alone. Verified on /meta and
/changelog: details content has the 1px warning border again.

fix(db): skip migrations a concurrent run already applied

October 24, 2026asce1062View on GitHub7873dcc
Commit details
  • check _migrations for each file inside its write transaction instead

of once up front, so the check is serialized by the write lock
  • test: two runs racing on one database apply each migration exactly

once (fails on the previous runner with "duplicate column name")

Two overlapping db:setup runs both saw 0001 as pending; the second hit
"duplicate column name" on ALTER TABLE and failed.

feat(guestbook): copy held entries only once they are approved

October 24, 2026asce1062View on GitHub16828f4
Commit details
  • store the guest's "send me a copy" opt-in as copyTheme (the site theme

the copy renders in), set only when an address was given; migration
0001 adds the column
  • a copy is sent only while the entry is visible: on submit for entries

that went live, on approval in /admin for entries the classifier held;
sendEntryCopy refuses anything not visible
  • deliverRequestedCopy claims copySentAt atomically before sending, so a

double-clicked Approve sends once; a failed send releases the claim so
a later approval retries
  • the admin result line says whether the requested copy went out

  • drop the pending/hidden variants of the copy email; every copy now

gets the view link plus the contact line
  • store the anchored submission time as the entry timestamp, so the row

matches the classification windows and the email idempotency key

The recipient address is never verified. Copying held entries at submit
let anyone mail content the classifier already distrusted from this
domain to any inbox; the moderator now gates every such email.

Deploy order: run npm run db:setup against production before deploying.
Without the column, guestbook reads and inserts fail.

Decision P2-5 from the site review.

feat(db): apply versioned migrations tracked in a _migrations table

October 24, 2026asce1062View on GitHub4232330
Commit details
  • add applyMigrations(client, dir): runs each db/migrations/NNNN_*.sql not

yet recorded, in filename order, inside a write transaction together
with its _migrations record, so a failure leaves neither half behind
  • db:setup now uses it; tests and the real-DB guestbook suite build their

databases through the same path
  • tests: apply-once idempotency, adopting a database created by the

baseline alone (production today), and rollback of a failed migration

The baseline is CREATE ... IF NOT EXISTS and could simply be re-run, but
schema changes such as ALTER TABLE ADD COLUMN cannot be repeated, so
applied files are now recorded instead of replayed.

refactor(admin): authenticate admin requests by session cookie only

October 24, 2026asce1062View on GitHub2ee4d6f
Commit details
  • remove bearer-token auth: checkAdminAuth(cookies) is now a synchronous

check of the signed session cookie; the raw token is verified with
argon2 only by the /admin login form, which then sets that cookie
  • update the middleware and both admin pages; tests now assert that a

session is the only credential and that checking it never runs argon2

Nothing used the Authorization: Bearer path, and every request carrying
that header cost an unthrottled argon2 verify (CPU amplification and an
unthrottled guessing endpoint next to the delayed login form).

Verified on an isolated dev server: the correct token sent as a Bearer
header now gets 303 -> /admin; form login + cookie still opens the
moderation page.

Decision P1-7 from the site review.

Merge branch 'housekeeping/site-polish'

October 24, 2026asce1062View on GitHubb480ab5
Commit details
Dependency upgrades, the phased site-wide review and polish:
  • security: admin auth bypass via percent-encoded paths, guestbook

input boundary (javascript: links, CSS injection, length caps), rate
limiting that never fired, music proxy limited to public playlists
  • service worker no longer serves the offline page for slashless URLs

  • accessibility: keyboard-operable nav drawer, customizer, avatar

generator, disclosures; focus handling in terminal and back-to-top
  • soft-navigation leaks in text effects, offline page, avatar, search

  • drafts excluded everywhere; SEO noindex/sitemap; privacy disclosures

  • Netlify caching, Node 24, install flags; pre-push type check

  • guestbook rainbow magical title and hearts


  • housekeeping/site-polish: (78 commits)

feat(guestbook): rainbow magical title and rainbow haunted hearts
fix(guestbook): restore the space between "and" and "color faded text"
fix(offline, nav): retry recovery after a failed redirect; DOM-order tab trap
fix(music): keep the last good catalog when a miss refresh fails
fix(music): readable long titles under reduced motion
fix(404): only set up the 404 neofetch widget on the 404 page
fix(seo): anchor the sitemap filter to top-level paths
fix(music): ignore superseded play() results; pause visualizer when hidden
fix(avatar): release generator and guestbook listeners when leaving
fix(neofetch): only touch the brand name on the page with the widget
fix(search): one Ctrl+K handler, no ?q= on other pages, scoped shortcuts
fix(content): real modified dates, word counts and search date sort
fix(seo): keep utility and admin pages out of search engines
docs(privacy): disclose sessionStorage keys and the offline cache
fix(pages): restore spaces lost where prose wraps before inline tags
fix(offline): stop polling when leaving /offline
fix(styles): reduced motion for animations, focus and forced colors
fix(content): exclude drafts from every page, list, feed and index
fix(a11y): make the 8biticon share control a button
docs(privacy): disclose every way the avatar-state key gets written
...

feat(guestbook): rainbow magical title and rainbow haunted hearts

October 24, 2026asce1062View on GitHubca63e4d
Commit details
  • the Guestbook h2 uses the declarative magical text attributes

(data-magical-text, a 7-color rainbow in data-magical-text-colors,
heart adornment); the global registry in Layout activates it
  • entry cards' Haunted heart creatures use the same rainbow palette

instead of primary/accent

Colors are literal hex values in the attributes, by choice.

fix(guestbook): restore the space between "and" and "color faded text"

October 24, 2026asce1062View on GitHub2eb9a92
Commit details
The line wrapped before the faded-text span, so Astro's HTML
compression rendered "andcolor faded text". Add the {" "} idiom.

fix(offline, nav): retry recovery after a failed redirect; DOM-order tab trap

October 24, 2026asce1062View on GitHub34968b4
Commit details
  • /offline: if the redirect to the last page fails, reset the guard,

show offline again and restart polling; before, recovery stopped for
good at "Redirecting..." (eb40c6c2 removed the old retry path)
  • mobile drawer Tab trap orders its stops by DOM position (the sidebar

precedes the header), so Tab from the hamburger no longer reaches the
header controls behind the scrim

Found by the final audit review. Verified: 50 mixed Tab/Shift+Tab
presses stay inside the open drawer.

fix(music): keep the last good catalog when a miss refresh fails

October 24, 2026asce1062View on GitHub3bed437
Commit details
isInPublicCatalog's early refresh replaced the cached catalog before the
load finished; a failed refresh then cleared the cache, so a transient
Navidrome error turned unknown-id 404s into 502s and dropped a valid
catalog. Load the refresh separately and only swap it in on success.

Found by the final audit review of d179b41f.

fix(music): readable long titles under reduced motion

October 24, 2026asce1062View on GitHubb9db41a
Commit details
The global reduced-motion rule (cd36ad3f) ends the now-playing marquee
after one instant pass, which left long titles clipped with no way to
read them. Under reduced motion truncate with an ellipsis instead, and
put the full title in the viewport's title whenever it overflows.

Found by the final audit review.

fix(404): only set up the 404 neofetch widget on the 404 page

October 24, 2026asce1062View on GitHuba1434a5
Commit details
The 404 page's astro:page-load listener outlives the page and set up
the neofetch widget on every later page, so after /404 -> /about the
About widget was bound twice (one dice click rendered twice, one setup
leaked). Return early unless [data-page-404] is present and tear down
on astro:before-swap. Completes 6ef56e6e (found by the final audit).

fix(seo): anchor the sitemap filter to top-level paths

October 24, 2026asce1062View on GitHubeb96024
Commit details
The filter matched any path segment, so /tags/meta/ (two entries use the
tag) was dropped from the sitemap, and any future tag named search, admin
or offline would be too. Anchor it to the first segment and add a test
with both kinds of path.

Found by the final audit review of 81b7f20b.

fix(music): ignore superseded play() results; pause visualizer when hidden

October 24, 2026asce1062View on GitHub6bce34f
Commit details
  • tag each playTrack with a request id and ignore a settled play() from

an older request: switching tracks rejects the previous play() with
AbortError, whose catch/finally marked the new track paused and
"playback blocked" and re-enabled controls while it was still loading
  • distinguish autoplay blocking (NotAllowedError) from a failed stream

instead of reporting both as "playback blocked"; AbortError is a no-op
  • AsciiVisualizerController.setVisible(): no analyser rAF loop or idle

interval while the player is minimized, closed or never opened; the
loop resumes for the current playback state when shown again

Verified against the real Navidrome: two rapid track clicks leave the
second track playing; rAF calls ~234 per 2s with the player open and 0
while minimized with audio still playing.

fix(avatar): release generator and guestbook listeners when leaving

October 24, 2026asce1062View on GitHub4aef7be
Commit details
  • AvatarGenerator: destroy() removes the controller's window

avatar-state-change listener; called before re-init and on
astro:before-swap. Each /8biticon visit left a controller (with an
image cache of every layer) re-rendering on every sidebar avatar change
  • guestbookAvatarWidget: abort its controller on astro:before-swap, not

only on the next guestbook init
  • guestbookCustomizer: the outside-click document listener is tied to a

per-visit AbortController (it accumulated per visit, holding detached
panel refs)

Verified: /8biticon -> /guestbook -> /about with sidebar randomizes, no
errors; the customizer still opens and closes on outside click.

fix(neofetch): only touch the brand name on the page with the widget

October 24, 2026asce1062View on GitHub6ef56e6
Commit details
neofetch.ts is imported by /about but its astro:page-load listener runs
for the rest of the session: every later page reset #nav-brand-name to
"alex" (overriding the sidebar egg's name) and set up the /404 page's
neofetch widget a second time. Return early unless the neofetch widget
is on the page and is not the 404 page's own.

fix(search): one Ctrl+K handler, no ?q= on other pages, scoped shortcuts

October 24, 2026asce1062View on GitHubaa9f630
Commit details
  • only the /search page box mirrors its query in ?q=; typing in the

floating modal rewrote every page's URL (/blog/x/?q=foo), so a reload
or shared link re-ran the search in a hidden input
  • FloatingSearch handles Ctrl/Cmd+K in one place: open when closed,

otherwise focus the input. The extra initSearchShortcut handler focused
the invisible modal input first, so openSearch recorded that hidden
input as the opener and closing put focus on it
  • only refocus an opener that is still in the document

  • /search binds its Ctrl+K and "/" shortcuts per visit, scoped to its own

box, and unbinds on swap; the module script runs once per session, so
its window.__*Cleanup pattern never ran and the handlers stayed on
every later page

Verified: Ctrl+K on /about opens the modal, typing keeps the URL clean,
Escape returns focus to the page; /search still updates ?q=; after
visiting /search, Ctrl+K on /about still opens the modal.

fix(content): real modified dates, word counts and search date sort

October 24, 2026asce1062View on GitHub9583e52
Commit details
  • pass updatedDate as dateModified, so article JSON-LD and

article:modified_time reflect edits (10 entries have updatedDate;
dateModified always equalled datePublished)
  • pass the remark plugin's exact word count to the JSON-LD instead of

reading minutes x 200
  • Pagefind date sort/meta read the date element itself:

data-pagefind-sort="date:{date}" was a quoted attribute, so the
literal "date:{date}" shipped and "sort by date" ordered by garbage;
the date meta also captured the reading time and description

Verified by build on /blog/2025-06-19-resume: sort value 2025-06-19,
dateModified 2026-03-17, wordCount 83.

fix(seo): keep utility and admin pages out of search engines

October 24, 2026asce1062View on GitHub81b7f20
Commit details
  • noindex /search, /404, /success, /offline, /admin and

/admin/guestbook (they rendered "index, follow")
  • sitemap filter excludes /404, /success, /search, /offline, /meta,

/admin and /.well-known paths; /meta was noindex yet listed, and the
admin pages were listed too

Verified by build: those pages carry "noindex, nofollow"; the sitemap
has 85 URLs with none of them, content pages (/about, /verify) intact.

docs(privacy): disclose sessionStorage keys and the offline cache

October 24, 2026asce1062View on GitHub2ad85cc
Commit details
The privacy page said the only things written to the browser are twelve
localStorage values, "not sessionStorage", and the PrivacyPolicy summary
and DNT policy said the same. The site also writes two sessionStorage
values (nav-brand-visited, offline_last_page: the previous page's
address) and its service worker keeps the site's pages in Cache Storage.

Describe both on /privacy, in the PrivacyPolicy summary, and in new
SESSION STORAGE and OFFLINE CACHE sections of dnt-policy.txt.

fix(pages): restore spaces lost where prose wraps before inline tags

October 24, 2026asce1062View on GitHub5b4acf5
Commit details
Astro's HTML compression drops the newline between a word and an inline
tag on the next source line, so readers saw "andThe First Descendant"
(interests), "Coversalex@ ... andcontact@" (verify), "persist
withtheme-flavor" (palette) and "emphasis,search result mark" plus two
token lists on /style. Add the {" "} idiom at each wrap point.

Also drop the duplicated ogImage prop on /success and /search.

fix(offline): stop polling when leaving /offline

October 24, 2026asce1062View on GitHubeb40c6c
Commit details
The /offline script discarded the teardowns returned by startPolling and
setupListeners. With soft navigation the module outlives the page, so
after leaving /offline the next successful poll (or an "online" event)
ran tryRedirectToLastPage and pulled the visitor away from whatever page
they had moved on to.

Set up on astro:page-load only when the offline page is present, tear
down polling, listeners and any pending redirect on astro:before-swap,
and redirect at most once per visit.

Verified: /offline -> soft nav to /about stays on /about; staying on
/offline still redirects once reachable.

fix(styles): reduced motion for animations, focus and forced colors

October 24, 2026asce1062View on GitHubcd36ad3
Commit details
  • the global prefers-reduced-motion rule also ends CSS animations after

one near-instant pass (elements and pseudo-elements) and disables
smooth scrolling; it only covered transitions, so .faded-text, the
dice spin, share-notification slides and the honeypot shake still ran
  • show a focus ring on the guestbook message textarea wrapper (the

textarea is outline: none and had no focus indicator at all)
  • .faded-text falls back to plain system text under forced-colors (the

transparent fill stayed while the gradient was dropped: invisible)
  • mobile nav scrim uses color-mix from black instead of an oklch literal

  • delete dead CSS: .nav-avatar-controls, #ascii-404, #expand-content p,

and the unused glow-pulse / glow-pulse-accent keyframes

fix(content): exclude drafts from every page, list, feed and index

October 24, 2026asce1062View on GitHub77a4d07
Commit details
  • add getPublishedEntries() in src/lib/content/published.ts, the only

reader of content collections; it drops entries with draft: true
  • use it in all 9 places that called getCollection directly: post and

note pages, blog/notes/tag indexes, prev/next (ContentLayout), RSS
and OG images
  • contract test: no source file outside the helper calls getCollection


The schema has a draft field, but nothing filtered on it: a draft would
have been built, listed, tagged, linked as prev/next, put in RSS, the
sitemap and the search index. No content is a draft today.

Verified by building with a planted draft: no page, no index/tag/OG/
sitemap entry.

fix(a11y): make the 8biticon share control a button

October 24, 2026asce1062View on GitHubb20f68a
Commit details
It was an <a> with no href, so it could not be focused or activated by
keyboard, and the aria-label on its role-less wrapper was ignored. Use
<button type="button" aria-label="Share avatar"> (the existing .icon
styles already cover a button) and role="group" on the wrapper.

Verified: Tab reaches it, Enter shares (notification shows); unfocused
it looks as before.

docs(privacy): disclose every way the avatar-state key gets written

October 24, 2026asce1062View on GitHub8769a24
Commit details
The privacy page, the PrivacyPolicy summary and the machine-readable
DNT policy said avatar-state is written only by the /8biticon builder.
It is also written by the sidebar avatar's save button and by ticking
"Include avatar" on the guestbook form. State all three, and reassert
the DNT policy date.

fix(avatar): validate 8biticon share-link params like stored avatars

October 24, 2026asce1062View on GitHub47a68d7
Commit details
  • ?gender= that differs from the current gender now starts from that

gender's defaults; it kept the other gender's layer values, e.g. a
saved male clothes 62 on the female set (59 options), which failed to
load and was silently rejected when saved
  • ?avatar= goes through parseAvatarState (integers in range, whole state

or nothing); "1.5-2-..." and "1e1" used to pass Number() and the range
check, producing 404 layer images and unsavable states

fix(guestbook): survive malformed #entry links and honour reduced motion

October 24, 2026asce1062View on GitHub1f59e07
Commit details
  • look the linked entry up with getElementById(decodeURIComponent(...))

instead of querySelector(hash); /guestbook#entry-a/b is an invalid
selector, threw, and aborted the rest of the guestbook init (send-copy
toggle, theme capture, share notification)
  • scroll to the linked entry without smooth scrolling, and mark it with

a static outline instead of the fading highlight, under
prefers-reduced-motion

Verified: /guestbook#entry-a/b loads with no errors; #entry-20 still
scrolls to and highlights the entry.

fix(a11y): make guestbook card customizer options keyboard operable

October 24, 2026asce1062View on GitHubcd6f476
Commit details
  • radius, border color and border style options are now

<button aria-pressed> with names ("Radius 1rem", "Accent",
"Border dashed") in role="group" sets labelled by their headings;
they were click-only <div>s and colors were conveyed by color alone
  • selectOption() keeps the .selected class and aria-pressed in sync for

clicks and for syncUI
  • the hover preview also runs on focus/blur

  • the pattern select and the width slider are labelled by their

headings; the slider exposes aria-valuetext ("1.5px")
  • buttons drop UA chrome and get a focus-visible ring


Verified by keyboard: open the panel, Enter on a radius, color and
style updates aria-pressed and the submitted style JSON; the panel
looks as before.

fix(a11y): make the Expand disclosure keyboard operable

October 24, 2026asce1062View on GitHubc5f4281
Commit details
The toggle was a clickable <h3>, so the guestbook customizer, the
privacy summary and the search tips could not be opened with a
keyboard. Keep the heading and put a full-width
<button aria-expanded aria-controls> inside it (disclosure pattern);
the body gets an id and aria-expanded follows the hidden state.

fix(a11y): make 8biticon layer choices keyboard operable

October 24, 2026asce1062View on GitHub81a8bda
Commit details
  • render each layer choice as <button type="button" aria-pressed> with

a name ("Clothes 12"); they were click-only <div>s, so keyboard and
screen reader users could not pick any face, clothes, hair, eyes,
mouth or background (only Randomize worked)
  • thumbnails and the number badge are aria-hidden (the button is named)

  • move the inline hover/selected styles to avatar.css and add a

focus-visible ring; no scale on hover under prefers-reduced-motion

Verified: Tab to a choice, Enter selects it and moves aria-pressed;
the grid looks as before.

fix(matrix): render the background crisply on HiDPI screens

October 24, 2026asce1062View on GitHub4a174dd
Commit details
  • size the canvas backing store by devicePixelRatio (capped at 2) and

draw in CSS pixels; it was innerWidth x innerHeight stretched by CSS,
so glyphs were blurry on 2x/3x displays
  • skip no-op resizes: reassigning width/height cleared the rain on every

mobile URL-bar or window-drag resize event
  • mark the canvas aria-hidden (decorative); reset the rAF id when tick

finds no canvas so the loop can restart
  • starsBackground: drop a MutationObserver on data-flavor whose handler

only toggles data-stars-bg (flavor changes never affect it)

Verified at deviceScaleFactor 2: canvas 1600x1200 for an 800x600 view,
glyphs crisp.

fix(effects): ignore unknown creature/adornment kinds instead of throwing

October 24, 2026asce1062View on GitHub7c9fd22
Commit details
  • hauntedRegistry and magicalTextRegistry accept data-haunted-creature

and data-magical-text-adornment only when the kind exists, else the
default applies; an unknown value threw a TypeError in the controller
and aborted initialisation of every later element on the page
  • mark the Haunted creature layer aria-hidden (some creature SVGs carry

no aria-hidden of their own)

fix(text-effects): release bindings and effects of swapped-out elements

October 24, 2026asce1062View on GitHub13e1f8e
Commit details
  • track bound elements and, on astro:after-swap, release every binding

whose element left the document: document listeners (resume,
route-enter, idle-return), IntersectionObservers, random-time timer
chains, and any effect still running (the looping 404 typewriter)
  • register a cleanup for random-interval timers (they had none)

  • never start an effect on a detached element (backstop)

  • glitch shimmer: one pending timer instead of an ever-growing array, a

post-effect stop that clearActiveEffect and the next transition call
(the shimmer outlived its own resolve, so cancel() could not reach it
and a second glitch overlapped it), and it stops once detached

Before: every soft navigation left ~35 document listeners, 2 observers
and 3 self-rescheduling timer chains per page bound to detached nodes,
which kept playing effects on them; work grew with session length.

Verified: after /404 -> /about, the 8 swapped-out effect elements get
0 DOM writes over 8s (the persisted music label still animates).

fix(terminal): one Escape per action, focus on minimize, safe Backspace

October 24, 2026asce1062View on GitHub46901e9
Commit details
  • stop propagation when the input handles Escape: the document-level

handler also ran, so closing the completion menu (or leaving
fullscreen) minimized the whole terminal in the same keypress
  • on minimize, move focus to the dock chip that restores the terminal

(or back to the opener) when focus was inside the window; hiding the
window used to drop focus to <body>. Only refocus an attached opener
  • Backspace typed while focus is outside the input deletes one

grapheme, not one UTF-16 unit (it left half an emoji in the command)
  • cap session command history at 100 and skip consecutive duplicates;

maxlength 256 on the input (echoed in full by the unknown-command
message)

Verified: Tab opens completions, Escape closes only the menu, a second
Escape minimizes with focus on the dock chip; "go😀" + Backspace from
the log leaves "go".

fix(navbrand): keep "more" and "details" in search queries

October 24, 2026asce1062View on GitHub7b66d66
Commit details
Verbose words were stripped from every input, so "search more" became a
bare "search" and "find details here" lost a word. After find, search or
lookup the rest is the query: only the explicit --verbose / -v flags
count as verbose there.

fix(navbrand): count one visit per session when sessionStorage is blocked

October 24, 2026asce1062View on GitHubb72c3d2
Commit details
Keep an in-memory "visit recorded" flag next to the sessionStorage one.
With storage blocked (private mode, disabled site data) the session flag
always read false, so every soft navigation ran onNewVisit: the visit
count went up per page, last-visit was overwritten and the arrival
greeting replayed.

Verified with sessionStorage throwing: three soft navigations leave the
visit count unchanged.

fix(navbrand): keep emoji whole under the terminal prompt cursor

October 24, 2026asce1062View on GitHubbd69c7d
Commit details
The prompt mirror took the cell under the block cursor with
afterCaret[0], a UTF-16 unit: with the caret before an emoji it painted
a lone surrogate (and the rest started with the other half). Take the
first grapheme instead, for the input and for the completion ghost text.

fix(navbrand): only run commands on exact or unambiguous prefixes

October 24, 2026asce1062View on GitHub7c8bf31
Commit details
  • drop the substring tier of the last-resort matcher: "ix" (inside the

alias "fix") reset the terminal, and other fragments could close it
or clear history
  • accept a prefix only when it is at least 2 characters and matches

exactly one command; ambiguous input falls through to the unknown
command message with suggestions instead of picking the first command
in registry order
  • the typed-suggestion path already used prefixes, so the ghost

completion and Enter now agree

fix(a11y): show sidebar option descriptions on keyboard focus

October 24, 2026asce1062View on GitHubd5b1503
Commit details
The helper text revealed on :focus-visible of the info icon, which has
no tabindex and can never take focus, so keyboard users on hover-capable
devices never saw it. Reveal it when the option's own control has
keyboard focus instead.

Verified: tabbing to an option shows its description.

fix(footer): drop the orphan bullet and name the Astro link

October 24, 2026asce1062View on GitHub24bd928
Commit details
  • move the bullet after the commit link inside its conditional, so

builds without COMMIT_REF (local, GitHub Pages mirror) no longer start
the line with "•"
  • give the icon-only Astro link an accessible name ("Built with Astro")

and hide the decorative icons from assistive tech

fix(header): link breadcrumb parents only when the page exists

October 24, 2026asce1062View on GitHubd4bc76c
Commit details
  • intermediate segments link only to sections listed in navigation.ts;

others render as text. /.well-known/change-password linked to a
/.well-known 404, and a 404 page at /foo/bar linked /foo and /foo/bar
  • mark the current page with aria-current="page" and expose the trail as

a Breadcrumb navigation landmark (role on the existing inline span, so
the layout is unchanged)

Verified: /blog/<post> and /tags/<tag> link their section; .well-known
and unknown 404 segments are text; the header looks the same.

perf(deploy): cache the hashed Workbox runtime as immutable

October 24, 2026asce1062View on GitHube1382c1
Commit details
workbox-<hash>.js lost its immutable header when the catch-all /*.js
rule was removed (ced99350). It is content-hashed, so it is safe to
cache forever.

chore(hooks): compare ascii art against HEAD and fail on generator errors

October 24, 2026asce1062View on GitHubbd58915
Commit details
  • fail the push when generate:ascii itself fails, instead of checking

an unchanged file and passing
  • diff against HEAD, not the index: the pushed commit is what matters,

and a staged-but-uncommitted regeneration must not pass

Found by the Phase 2-4 audit review of 793dfebc.

fix(burn404): rebuild the fire palette on any theme change

October 24, 2026asce1062View on GitHub2b959a2
Commit details
Observe data-theme instead of listening for "set-theme". Since f43316f9,
URL theme overrides (?theme=) paint without dispatching that event, so
/404?theme=light over a saved dark theme drew the dark-theme fire.

Found by the Phase 2-4 audit review. Verified: /404?theme=light now
renders the same light palette as a saved light theme.

fix(music): keep the public catalog consistent and resilient

October 24, 2026asce1062View on GitHubd179b41
Commit details
  • load playlist details with Promise.allSettled: one failing playlist

is logged and left out instead of failing the whole catalog (every
music route returned 502 until a full load succeeded)
  • serve /api/music/playlists from the catalog itself, so the list never

offers a playlist, track or cover the other routes would refuse
  • isInPublicCatalog(): on a miss, reload the catalog once (at most every

30s) before answering 404, so a playlist just made public or a cover
id changed by an edit is not refused for the 5-minute TTL
  • forward If-None-Match / If-Modified-Since for covers and pass 304

through; abort upstream at once if the request is already aborted

Found by the Phase 2-4 audit review of a659fdaf. Verified against the
real Navidrome: 10 playlists, 206 stream, cover 200.

fix(a11y): take the hidden sidebar brand block out of the tab order

October 24, 2026asce1062View on GitHubdd91ec1
Commit details
  • hide the inactive brand variant with visibility as well as opacity,

max-height and pointer-events; visibility switches instantly when a
side appears and waits for the fade when it disappears, so the
crossfade is unchanged
  • drop aria-hidden="true" from the collapsed brand container: it wrapped

a focusable role="button" (axe aria-hidden-focus), and visibility now
hides it from assistive tech when it is not shown

Before: in the expanded sidebar the invisible collapsed-avatar trigger
was a tab stop; in the collapsed sidebar the invisible prompt button was.
Verified in both states: only the visible variant is focusable;
screenshots match the previous look.

fix(a11y): make the mobile nav drawer keyboard operable

October 24, 2026asce1062View on GitHub360b38f
Commit details
  • hamburger is a <button aria-controls="nav-menu" aria-expanded> that

toggles the existing #nav-toggle checkbox (still the CSS state);
it was a <label> for a display:none checkbox, so keyboard and switch
users could not open the drawer, and Escape "returned focus" to an
element that cannot take focus
  • below 1024px the closed drawer is inert (its ~35 off-canvas links were

tab stops); opening moves focus into it and Tab is trapped between the
hamburger and the drawer until Escape, a link or the overlay closes it
  • drop role="navigation" + aria-label from the <aside>: it nested a

second "Main navigation" landmark around the real <nav>
  • sidebarCollapse: handle Escape only on desktop; at mobile widths the

same Escape that closed the drawer also cleared the saved collapse
preference

Verified at 390px with the keyboard: closed drawer has no tab stops,
Enter opens with focus inside, 60 Tabs stay inside, Escape closes and
returns focus, overlay click syncs aria-expanded; desktop not inert.

refactor: remove unused theme, shortcut and clipboard exports

October 24, 2026asce1062View on GitHubdcbc25a
Commit details
  • themeManager: toggleTheme (no callers; also skipped disabling

match-device mode, unlike handleThemeToggle) and resolveActiveTheme
  • keyboardShortcuts: registerShortcuts

  • copyToClipboard: the CopyToClipboard class and its options type


knip still reports eight exports of the unwired transition-picker
system, kept until the finish-or-remove decision is made.

fix(runtime): stop tab-title interval leaks and NaN scroll progress

October 24, 2026asce1062View on GitHub6f4f2c8
Commit details
  • tabTitle: clear any running rotation before starting a new one; a

second "hidden" without "visible" (bfcache) orphaned an interval that
rewrote document.title forever
  • scrollProgress: 0% on pages shorter than the viewport (was NaN%), and

no width transition under prefers-reduced-motion; remove the unused
removeProgressBar
  • Layout: fix a comment that labelled the console egg script as scroll

progress

fix(iframe): follow every theme change without reloading the iframe

October 24, 2026asce1062View on GitHub46e758c
Commit details
  • only assign iframe.src when the themed URL actually differs; every

assignment restarts navigation, so each page load and theme event
reloaded the GitHub contributions / resume iframe
  • observe data-theme instead of the "set-theme" event, which URL theme

overrides no longer dispatch (f43316f9)
  • drop initMultipleIframeThemes and cleanupIframeTheme (unused; the

cleanup built fresh closures and removed nothing)
  • add clientRuntime.dom.test.ts (jsdom) for both, plus back-to-top

fix(a11y): keep back-to-top focusable and honour reduced motion

October 24, 2026asce1062View on GitHub906fb72
Commit details
  • cancel the auto-hide while the button has focus (focusin/focusout),

not only on hover; hiding a focused button dropped keyboard focus
to the body
  • after scrolling up, move focus to #main-content (tabindex -1) so the

keyboard user continues from the top of the page
  • scroll with behavior "auto" under prefers-reduced-motion

  • no focus ring on #main-content: it is a programmatic focus target

(skip link, back-to-top), not a control

fix(music): serve only public playlists, their tracks and covers

October 24, 2026asce1062View on GitHuba659fda
Commit details
  • add getPublicCatalog() in navidrome.ts: ids of public playlists, the

tracks they contain and the covers they use, cached per warm function
instance for 5 minutes (failed loads are not cached)
  • /api/music/playlist/[id], /stream/[id] and /cover/[id] now 404 any id

outside that catalog
  • delete /api/music/albums: unused by the player, and it listed the

whole album library unfiltered
  • validate ids against /^[A-Za-z0-9_.-]{1,128}$/ before any upstream call

  • stop returning upstream and config errors ("Wrong username or

password", missing env var names) to the browser; log them and answer
"Music service unavailable" (502/503) or "timed out" (504)
  • time out JSON calls (8s) and media until headers arrive (8s), and

abort the upstream stream when the browser disconnects
  • covers: public, max-age=86400 plus ETag/Last-Modified; pass 404/416

through; nosniff on media responses
  • add musicApi.test.ts route tests against a fake Navidrome


Before: the playlists list hid private playlists, but any private
playlist was readable by id, and stream/cover proxied any id in the
library with the server's credentials.

Verified against the real Navidrome on an isolated dev server: 10
public playlists, playlist detail, 206 range stream, cacheable covers;
unknown ids 404, malformed id 400, /albums 404; in-browser playback OK.

fix(theme): stop ?theme= links from overwriting the saved theme

October 24, 2026asce1062View on GitHubf43316f
Commit details
  • setTheme(theme, false) now paints data-theme and color-scheme directly

instead of dispatching "set-theme": astro-themes' handler stores
whatever that event carries, so persist:false still saved the URL value
  • add paintTheme() and use it for the after-swap URL/match-device

restamp, which previously set data-theme but left color-scheme on the
stored theme (mismatched scrollbars and form controls)
  • add a test that a URL theme fires no "set-theme" and leaves storage


Before: a visitor whose saved theme was dark who opened a ?theme=light
link (embed, screenshot, share) had their saved preference silently
switched to light on every later visit.

Verified in the browser: /?theme=light paints light with the saved
dark intact; plain / is dark again; the manual toggle still persists.

chore(types): type-check and lint markdown.config.ts and vitest.config.ts

October 24, 2026asce1062View on GitHub70bd9e4
Commit details
  • add both to tsconfig include; stop ignoring *.config.ts in eslint

  • replace the any-typed remarkReadingTime signature with precise types

  • cast the three rehype plugins whose Node->Node transformer types do

not satisfy Astro's Root->Root signature to the unified() plugin
entry type, instead of any (and without the deprecated
markdown.rehypePlugins type)

markdown.config.ts holds real logic but was outside astro check and
eslint. Verified by build: reading time, heading ids/TOC (27 posts),
external-link icons and accessible emoji all still render.

fix(og): build the OG image path without assuming a trailing slash

October 24, 2026asce1062View on GitHub3b9663f
Commit details
  • strip a trailing slash only when present; slice(0, -1) cut the last

character of slashless URLs (dev server)
  • drop the no-op .replace(".mdx", "") in the OG route: glob loader

ids carry no extension

test(content): enforce permalink == filename for blog and notes

October 24, 2026asce1062View on GitHub5c99633
Commit details
The schema accepts any non-empty permalink, but prev/next navigation and
share links use it while URLs and OG images use the file id, so a typo
diverges silently. Verified the test fails on a planted typo.

chore(knip): let knip report unused components, layouts and scripts

October 24, 2026asce1062View on GitHube4f20a1
Commit details
  • stop listing every component, layout and client script as a knip

entry; they are now reached through pages, so orphans get reported
  • load musicPlayerModal via <script> import like every other component;

knip cannot follow <script src>, and it was the only one using it
  • keep MagicalText.astro as an explicit entry: built but not yet placed

on any page (decision pending)

knip now reports 15 unused exports in the client runtime; they are
removed with the Phase 4 dead-code fixes.
Verified: music player opens from its trigger, no console errors.

chore(hooks): fail pre-push on stale ascii art and type errors

October 24, 2026asce1062View on GitHub793dfeb
Commit details
  • regenerate src/data/ascii-art.json and fail with a message when it

differs from the committed file, instead of git-adding it: staging
in pre-push never makes it into the push and left the tree dirty
  • run astro check before every push; vitest strips types, so a type

error in a test (bb465efe) passed the pre-commit hook

Verified: clean tree passes; a stale committed file fails with exit 1.

docs(well-known): describe the admin area on change-password

October 24, 2026asce1062View on GitHubb9ef832
Commit details
The page claimed the site has no authenticated section; /admin exists
(owner-only token login). State that there are no user accounts or
passwords to change instead.

fix(deploy): correct Netlify caching, Node version and install flags

October 24, 2026asce1062View on GitHubced9935
Commit details
  • drop the catch-all immutable rules for /*.js and /*.css; they also

hit unhashed /sw.js and /registerSW.js. Serve both with no-cache.
Hashed build assets keep immutable via /astro/*
  • NODE_VERSION 22 -> 24 to match local development; add package.json

engines (">=22.23.4", Astro's floor)
  • remove NPM_FLAGS="--legacy-peer-deps"; the lockfile installs cleanly

with a plain npm ci, and the flag resolved peers differently from
local installs
  • remove @netlify/plugin-sitemap: @astrojs/sitemap already emits

sitemap-index.xml, and the plugin's sitemap.xml would shadow the
/sitemap.xml redirect with a second, unfiltered sitemap
  • delete public/_headers (duplicated netlify.toml content types) and

the unsigned root public/security.txt; redirect /security.txt to
/.well-known/security.txt, the copy the signature covers
  • add src/tests/deployConfig.test.ts contracts for all of the above

plus the IcoMoon token

Verified: clean npm ci of this lockfile without the flag (exit 0).

fix(fonts): version the IcoMoon stylesheet URL

October 24, 2026asce1062View on GitHubddd3eb1
Commit details
  • load /fonts/icomoon/style.css?<token> with the same token its font

URLs use, from one ICOMOON_TOKEN constant shared with the preload

/fonts/* is served immutable for a year, but the stylesheet URL never
changed. After the 2026-10-22 IcoMoon regeneration (7 new icons),
returning visitors kept the old stylesheet and the new icon classes
rendered as nothing. A contract test (next commit) keeps the Layout
token in sync with style.css.

fix(email): bound delivery time and retry real network failures

October 24, 2026asce1062View on GitHubafacc94
Commit details
  • retry undici's TypeError("fetch failed") and read the TCP/DNS code

from err.cause; the old pattern only checked err.message, so real
network failures never retried and logged only "fetch failed"
  • keep the abort timer running until the response body is read, so a

stalled error body cannot hang the guestbook POST
  • add a total budget across attempts and backoff

(GUESTBOOK_EMAIL_DEADLINE_MS, default 6s, max 8s; per-attempt default
4s), since the POST awaits delivery inside a 10s serverless limit
  • parse tuning env vars so "0" is honoured; GUESTBOOK_EMAIL_RETRIES=0

used to fall back to 2 via Number(x) || 2
  • send the admin notification and entry copy in parallel, each with its

own error handling, so a failed send or delivery-record write never
skips the other; log the reason when a send does not go out
  • drop getNotificationFlags: it read flags on a row inserted a moment

earlier, so they were always null; dedup comes from the Idempotency-Key

Before: worst case was ~31s per email, sequentially, after the entry was
already inserted. The function would time out, the visitor would see an
error and resubmit, creating a duplicate entry.

Verified on an isolated dev server: POST redirects in ~50ms with both
failures logged (no Resend key configured there). Suite run 3x clean.

fix(guestbook): tell people why a submission was rejected

October 24, 2026asce1062View on GitHubdf9913f
Commit details
  • set maxlength on name, email, url and message from ENTRY_LIMITS so

the browser enforces the server limits
  • when the server rejects a submission (not a bot), answer 400 and show

an error notice above the form instead of silently re-rendering it;
honeypot hits still get the plain page
  • re-validate the stored avatar state at render (legacy rows) and pass

the canonical value to the canvas and entry actions

Found by the Phase 1 audit review: the 100-character name cap from
94f3724b was enforced with no feedback.

fix(guestbook): treat slash-separated tag attributes as html_tag

October 24, 2026asce1062View on GitHub66e43e4
Commit details
  • accept whitespace or / after the tag name, so <svg/onload=...> and

<img/src=x ...> are hard-hidden again (the previous /<[^>]+>/ caught
them; the narrowed regex in 99487a49 did not)
  • if an entry is deleted between the read and the moderation batch,

return false instead of surfacing the foreign-key failure as a 500

Found by the Phase 1 audit review.

fix(email): keep user fields on one line and truncate by code point

October 24, 2026asce1062View on GitHubaeb5110
Commit details
  • add singleLine(): line breaks (CR/LF, NEL, U+2028/U+2029), C0/C1

controls and bidi overrides/isolates collapse or drop; use it for
name and email in both plain-text bodies so a field cannot forge a
"Status:" or "Review:" line
  • sanitizeHeaderValue and parseEmailAddress now cover the same

characters, not only CR/LF, and truncate by code point
  • truncate() cuts on code points; a split surrogate pair serializes as

a lone "\ud83d" that strict JSON parsers can reject
  • HTML templates use the same sanitizeUserText + truncate as the text

part; preheaders no longer slice mid-emoji
  • build the avatar image URL with URLSearchParams

  • render the "open a GitHub issue" link only when the URL is known

fix: escape RSS image attributes and reject padded admin tokens

October 24, 2026asce1062View on GitHub462b342
Commit details
  • escape the image src and alt interpolated into each RSS item's HTML

description; fall back to the build time for lastBuildDate when the
blog collection is empty instead of emitting "undefined"
  • hash-admin-token: reject tokens with leading or trailing whitespace

(the login trims input, so such a token could never log in) and warn
on stderr that the argv form leaks the token into shell history and
ps; stdout still carries only the hash

fix(admin): redirect after moderation POST and whitelist the filter

October 24, 2026asce1062View on GitHub17e0064
Commit details
  • answer moderation POSTs with 303 to /admin/guestbook?filter=&result=&entry=

so reloading the page cannot repeat the action
  • show "no longer exists" when the entry id is unknown

  • validate the filter query against the known values (unknown -> all)

instead of casting it

Verified on an isolated dev server (local SQLite, throwaway admin
token): hide -> 303 -> "Entry #21 hidden.", unknown id -> "no longer
exists", ?filter=bogus -> 200 with all entries, two audit rows written.

fix(guestbook): make moderation updates atomic and reject unknown ids

October 24, 2026asce1062View on GitHub250bdb9
Commit details
  • return false from updateEntryStatus when the entry does not exist,

instead of inserting an audit row that fails the foreign key (500)
  • write the status update and its audit row in one db.batch so they

commit together
  • add guestbook.db.test.ts: runs guestbook.ts against a real libsql

database from the baseline migration (duplicate + rate-limit through
createGuestEntry, moderation update and audit row, unknown id)

The existing guestbook tests mock the DB, which is why the timestamp
format bug never surfaced. The new suite covers the real query path.

perf(admin): check the session cookie before any argon2 verify

October 24, 2026asce1062View on GitHub0153b24
Commit details
  • validate the HMAC session cookie first in checkAdminAuth and only

verify a bearer token when no valid cookie is present
  • add a test that a valid cookie never triggers argon2


The middleware calls checkAdminAuth on every /admin* request. With the
bearer check first, any request carrying an Authorization header paid
a full argon2 verify even for a logged-in admin.

fix(guestbook): catch markup in names and stop hiding "<3" messages

October 24, 2026asce1062View on GitHub99487a4
Commit details
  • require a tag-shaped match for html_tag (<, optional /, letter-led

name, then > or attributes); "I <3 this, 5 > 4" was auto-hidden
  • run html_tag, bbcode, obfuscated_link and url_shortener over the name

as well as the message, and url_shortener over the url field
  • bump CURRENT_MODERATION_VERSION to 2 for the changed ruleset


The name is rendered publicly beside the message but skipped every
hard check, so "<b>Buy now</b>" or "bit.ly/xyz" as a name published as
visible. The old /<[^>]+>/ matched any "<" followed later by ">", which
hid ordinary prose with no review.

fix(guestbook): validate entry url, style and avatar at one boundary

October 24, 2026asce1062View on GitHub94f3724
Commit details
  • add src/lib/api/guestbook-input.ts: readGuestbookForm, sanitizeEntryUrl

(http/https only, normalized href), parseEntryStyle (customizer
allowlists only), normalizeAvatarState, and field length limits
  • use it in the POST handler (missing fields no longer throw; a

non-form body returns 400), in createGuestEntry before insert, and
again at render time in GuestbookEntry and the admin page for rows
stored before the boundary existed
  • show unsafe stored URLs on the admin page as inert text, not links

  • replace the email helpers' looser parseEntryStyle with the shared one

(also closes the prototype-key palette lookup) and have sanitizeUrl
return the normalized href so plain-text emails cannot gain lines

Before: url only had a client-side pattern, so a POSTed
javascript: URL was stored and rendered as a clickable href on the
public page and on the admin page (script on the admin origin). The
style JSON was spread unvalidated into the inline style attribute, so
an entry could overlay the page or load third-party images. name,
email, url, style and avatarState had no length caps.

Verified on an isolated dev server (local SQLite, no email): hostile
POST stores url NULL, a defaulted borderWidth and avatarState NULL; a
hostile legacy row renders with no javascript: href and default style;
non-form POST returns 400.

fix(guestbook): store ISO timestamps so rate limits and duplicates fire

October 24, 2026asce1062View on GitHub87fb587
Commit details
  • replace .default(sqlCURRENT_TIMESTAMP) with $defaultFn(() => new Date())

on Guestbook.timestamp and GuestbookModerationLog.at
  • add a libsql integration test that inserts without a timestamp and

queries a same-day window

Drizzle emitted CURRENT_TIMESTAMP for omitted timestamps, which SQLite
stores as "YYYY-MM-DD HH:MM:SS". Window bounds are bound as ISO
("...T...Z") and compared as text; " " sorts before "T", so every row
inserted the same UTC day fell outside the 10-minute rate-limit and
1-hour duplicate windows. Spam bursts were never rate_limited or
flagged duplicate.

The migration SQL default is unchanged (inserts outside Drizzle). Rows
already stored in the old format are older than any window.

fix(pwa): serve the offline page only when navigation fails

October 24, 2026asce1062View on GitHubc4fac52
Commit details
  • replace workbox navigateFallback with a NetworkOnly navigation route

that uses PrecacheFallbackPlugin for /offline/index.html
  • set navigateFallback to null explicitly; vite-plugin-pwa defaults a

missing value to "index.html", which would serve the home page instead
  • drop the hand-maintained navigateFallbackDenylist (redirects, SSR

routes and feeds now reach the network by construction)
  • update the Astro 7 contract test for the new fallback shape


navigateFallback is an app-shell setting: once the service worker was
installed it answered every navigation missing from the precache with
the offline page, even while online. Precache keys are dir/index.html,
so every slashless URL (/about, /blog, /now: all sidebar hrefs), the
/sps redirect and any future redirect missing from the denylist showed
"Offline" on reload, new tab, bookmark or shared link.

Verified on a built dist with an active worker: online /about, /blog,
/now redirect to their pages, unknown paths get the server 404; offline
precached pages load from cache and /guestbook shows the offline page.

fix(security): enforce admin auth on percent-encoded admin paths

October 24, 2026asce1062View on GitHub44df599
Commit details
  • decide admin-ness from context.routePattern as well as the raw

pathname, so /%61dmin/guestbook and /%2561dmin/guestbook get the
CSRF check, auth redirect and no-store headers
  • re-check admin auth inside admin/guestbook.astro as defence in depth

  • add middleware regression tests for encoded and double-encoded paths


Astro decodes the pathname repeatedly before routing, but the
middleware tested the raw URL pathname. An unauthenticated GET to
/%61dmin/guestbook rendered the full moderation page (entries,
emails, moderation actions) with no Cache-Control, both locally and
on production (verified read-only: 200 "Guestbook Moderation").

Verified: dev server returns 303 -> /admin with no-store for all
encoded variants; /, /about/, /administer unaffected; middleware
tests 23/23; astro check 0 errors.

fix(test): type burn404 dom canvas stub against getContext signature

October 24, 2026asce1062View on GitHubbb465ef
Commit details
  • mock getContext with mockImplementation typed as the full method

signature instead of mockReturnValue
  • mockReturnValue resolved to the last getContext overload

(GPUCanvasContext), so astro check failed with ts(2345)
  • vitest strips types, so the suite passed while astro check failed


Verified: astro check 0 errors; burn404.dom.test.ts 7/7 pass.

chore(deps): refresh lockfile metadata for npm 11.19

October 24, 2026asce1062View on GitHub84d02e5
Commit details
npm 11.19.1 (bundled with Node 24.22.1, now the local default) no longer
marks the optional pg-cloudflare package as dev-only. Metadata only; no
version or resolution changes.

test: add jsdom environment and burn404 soft-navigation regression tests

October 24, 2026asce1062View on GitHub6c542dd
Commit details
  • add jsdom as a dev dependency; tests opt in per file with

// @vitest-environment jsdom, so existing node-env suites keep their
speed and isolation
  • cover the burn404 soft-navigation lifecycle: both fire canvases are sized

on first load and again after 404 -> away -> back, and the torch mutation
observer rebinds to the new 404 article
  • cover resolveFlammableAnchor and containsFlammableAnchor anchor resolution


The lifecycle tests were run against the pre-fix burn404.ts and
burn404torch.ts from main: both soft-nav regressions fail there and pass on
this branch. jsdom has no canvas backend or layout engine, so the file stubs
a minimal 2D context, matchMedia, document.fonts, requestAnimationFrame, and
Range#getClientRects. The last one stays installed file-wide because the
torch's debounced detection can fire after a test ends.

chore(test): use import.meta.dirname in vitest config

October 24, 2026asce1062View on GitHub02399bf
Commit details
Vite flags __dirname as unsupported by the native config loader planned as
the default in its next major. import.meta.dirname is the ESM equivalent.

style: apply prettier-plugin-astro 1 and prettier-plugin-toml 4 formatting

October 24, 2026asce1062View on GitHubad1edfa
Commit details
Mechanical reformat of tracked files after the formatter major upgrades in
the previous commit. lint-staged would otherwise apply it piecemeal to every
file a future commit touches, mixing format churn into unrelated diffs.

  • prettier-plugin-astro 1.x breaks inline elements onto separate lines and

writes explicit {" "} separators where whitespace is significant
  • prettier-plugin-toml 4.x adds blank lines between netlify.toml tables


No rendered output changes: both builds were compared page by page (93 pages)
on visible text with whitespace preserved and on <pre> content verbatim,
with zero differences. The comparison was confirmed to flag a single dropped
space when one was planted.

chore(deps): upgrade dependencies and patch netlify-cli toml advisory

October 24, 2026asce1062View on GitHub2269882
Commit details
  • upgrade all dependencies to latest via ncu -u, with a clean reinstall

(node_modules and package-lock.json regenerated)
  • notable majors: @astrojs/mdx 8, vite-plugin-pwa 2, vitest 5,

prettier-plugin-astro 1, prettier-plugin-toml 4, netlify-cli 27
  • keep typescript at 6.0.3: 7.0.2 is outside the peer ranges of

@astrojs/check (^5 || ^6) and typescript-eslint (<6.1.0), so npm install
fails with ERESOLVE; forcing it would run astro check and lint on an
unsupported compiler
  • override toml to ^5.0.1: netlify-cli 27 still pins toml 4.3.0 (three

high-severity parse DoS advisories). 5.0.1 is patched, and netlify-cli's own
zip-it-and-ship-it already uses it with the same default-import API. npm
audit's suggested fix downgraded netlify-cli to 5.3.1.

Verified: npm audit reports 0 vulnerabilities; astro check, eslint, knip,
and 567 tests pass; production build succeeds and the generated service
worker still precaches offline/index.html and binds the fallback to
/offline/index.html; dev server pages load with clean consoles.

Local Node 24.2.0 is below the ^24.16.0 engine range of the new
eslint-plugin-astro/astro-eslint-parser (EBADENGINE warning only).

chore(git): ignore Playwright MCP browser artifacts

October 24, 2026asce1062View on GitHub5ff25c8

fix(burn404): keep torch burns through text effects, scroll, and soft nav

October 24, 2026asce1062View on GitHubd19289a
Commit details
The torch stored burns as viewport cells and cleared all of them on any DOM
mutation in the 404 article, on scroll, and on sidebar collapse. Every
text-effect tick (typewriter, glitch shimmer, ASCII dice) therefore wiped
burns across the whole page, not just the characters it changed.

  • add burn404flammable.ts: flammable ink and burn memory are stored per

anchor (nearest h1-h6/p/pre/li/a/hr) relative to its origin cell, with
origins read each frame so burns travel with their text
  • re-detect only the anchors a mutation touched; a full re-detect runs only

for structural changes, resize, and sidebar toggles
  • mask burn memory by current ink: a removed glyph stops burning and

re-ignites when ink returns to that spot
  • drop an anchor's burns only when a full re-detect finds its width changed

(glyphs reflowed); anchors with no ink right now keep their memory
  • remove the scroll listener; guard queued detection with a generation

counter; grow burn sets in place and skip off-screen anchors per frame
  • fix soft navigation back to the 404 (pre-existing): both burn404 canvases

kept stale _fireW, so the new canvas stayed at the 300x150 default, and the
mutation observer stayed bound to the detached article

Verified in the browser at 1280x900 and 390x844: a burnt heading holds
~760 lit cells for 12s of typewriter and glitch activity (previously 0
after ~8s), follows scroll, survives typewriter backspace/retype, and both
canvases size correctly after two 404 -> /blog -> back round trips.

fix(text-effects): animate by grapheme so emoji never render broken

October 24, 2026asce1062View on GitHubb7dd51b
Commit details
Every text effect indexed strings by UTF-16 code unit (split(""), text[i],
slice(0, i)). The typewriter therefore painted half of a surrogate pair for
one tick, so "...set the page on fire 🔥" on the 404 page flashed U+FFFD
before the emoji settled. The same latent bug existed in every other effect
and in charset pickers given emoji items.

  • add splitGraphemes() (Intl.Segmenter, Array.from fallback) and

pickRandomGrapheme() to textEffects/utils
  • route all 14 renderers, renderTypingFrame, and charset/items pickers

through grapheme arrays; ASCII behavior is unchanged
  • add a renderer sweep test that records every textContent write for text

with a surrogate pair and a ZWJ sequence and asserts no frame splits one

Verified in the browser: sampling the 404 typewriter every 40ms across a
full cycle shows no lone surrogates (previously "\ud83d█" was observed).

Merge branch 'docs/sps-operator-controls'

October 22, 2026asce1062View on GitHub378dad7
Commit details
  • docs/sps-operator-controls:

docs(sps): update SPS policies for operator-controls launch

docs(sps): update SPS policies for operator-controls launch

October 22, 2026asce1062View on GitHub034364a
Commit details
Describe live status notices and operator-enabled feedback, control,
review, and export instead of a not-yet-operational interface. Add the
implemented retention schedule, storage protections (per-user ACLs,
DPAPI for the bot token and action payloads), export, correction, and
scoped erasure procedures, and a reviews-and-acceptance section in the
terms.

Merge branch 'docs/sps-policy-language'

October 22, 2026asce1062View on GitHub9bd5250
Commit details
  • docs/sps-policy-language:

docs(sps): use neutral policy voice in SPS terms and privacy

docs(sps): use neutral policy voice in SPS terms and privacy

October 22, 2026asce1062View on GitHubd600d33
Commit details
Replaces first-person wording with defined roles: "the operator"
(the person who runs SPS) and "authorized users" (people allowed to
use the Discord interface). Storage location now reads as the computer
where SPS runs. No change to what the pages commit to.

Merge branch 'feature/sps-policy-pages'

October 22, 2026asce1062View on GitHubdb423c3
Commit details
  • feature/sps-policy-pages:

feat(sps): add Self Prompting System terms and privacy pages
chore(icons): add policy icons to icomoon set

feat(sps): add Self Prompting System terms and privacy pages

October 22, 2026asce1062View on GitHubd01a14c
Commit details
Adds /sps/terms/ and /sps/privacy/ for the private SPS Discord
application, cross-linked and indexed. Both state that the Discord
interface is planned and not yet operational, and describe intended
data handling without claiming unestablished storage, retention or
provider guarantees.

The website privacy page gains a scoping line linking the separate
SPS policy; existing website and guestbook commitments are unchanged.
/sps redirects (302) to /sps/terms/ because the header breadcrumb
links that segment.

chore(icons): add policy icons to icomoon set

October 22, 2026asce1062View on GitHub90a4cc9
Commit details
Adds exclamation-triangle, robot, person-check, clock-history, hdd,
database and file-earmark-text, and updates the font cache-bust token
in the Layout preload. Keeps woff ahead of ttf in the @font-face src
list so the preloaded woff is the file browsers use.

Merge branch 'feat/motif-attachments'

August 6, 2026asce1062View on GitHubd672561
Commit details
  • feat/motif-attachments:

feat(motifs): add waving-hand creature and granular Haunted glow/creature toggles

feat(motifs): add waving-hand creature and granular Haunted glow/creature toggles

August 6, 2026asce1062View on GitHub2979a22
Commit details
  • Register a multi-tone waving-hand SVG in the shared adornments registry

and wire it to the hello page's "Hello." header via data-magical-text
  • Add disableGlow and disableCreatures to Haunted, independent of

disableFun, exposed through both the component props and the
data-haunted-* attributes
  • Fix MagicalText re-inserting captured markup via textContent instead of

innerHTML, which rendered raw tag syntax for elements with child markup
(e.g. an icon) instead of parsing it
  • Wrap GuestbookEntry bubbles in Haunted (heart creature, glow disabled)

and scope the guestbook content-stacking CSS rule to .haunted-content so
it doesn't fight Haunted's creature overlay
  • Add typewriter, glitch, and signal-loss text effects to the 404 page

headings

Merge branch 'fix/pwa-offline-fallback'

July 22, 2026asce1062View on GitHub38516cf
Commit details
  • fix/pwa-offline-fallback:

fix(pwa): align offline fallback with precache

fix(pwa): align offline fallback with precache

July 22, 2026asce1062View on GitHuba04fb50
Commit details
Point the production Workbox navigation fallback at the generated offline/index.html document while retaining the clean /offline route in development.

Add an Astro 7 contract test that prevents the production and development fallback paths from drifting back together.

Merge branch 'fix/header-cursor-spacing'

July 22, 2026asce1062View on GitHubb8327ea
Commit details
  • fix/header-cursor-spacing:

fix(header): refine cursor and greeting scale

fix(header): refine cursor and greeting scale

July 22, 2026asce1062View on GitHubd1aa762
Commit details
  • remove the legacy negative margin so Astro 7 renders the blinking cursor flush with the final breadcrumb character

  • increase the site greeting size

Merge branch 'feat/sidebar-active-route'

July 22, 2026asce1062View on GitHubfb982da
Commit details
  • feat/sidebar-active-route:

feat(sidebar): add Workbench nav selection states

feat(sidebar): add Workbench nav selection states

July 22, 2026asce1062View on GitHubdc51444
Commit details
  • replace the active-route border and glow with animated marching ants

  • add a semantic checkerboard stipple for hover and active-hover states

  • share the treatments across expanded, collapsed, and mobile navigation

  • preserve static selection borders for reduced-motion preferences

Merge branch 'fix/pwa-production-dependency'

July 22, 2026asce1062View on GitHube4f3062
Commit details
  • fix/pwa-production-dependency:

fix(pwa): install build plugin in production

fix(pwa): install build plugin in production

July 22, 2026asce1062View on GitHub4b6ad5e
Commit details
  • promote vite-plugin-pwa from devDependencies because Astro imports it while loading the production build config

  • refresh lockfile production metadata for the PWA and Workbox dependency tree

  • add an Astro 7 contract test that prevents the plugin from returning to development-only dependencies

Merge branch 'update'

July 22, 2026asce1062View on GitHub57df4bf
Commit details
  • update:

build(astro): complete v7 compliance migration
build: upgrade Astro stack and preserve PWA
fix: honor manual theme toggle for dark Shiki code blocks

build(astro): complete v7 compliance migration

July 22, 2026asce1062View on GitHub4a81569
Commit details
  • replace the removed Astro DB integration with direct Drizzle/libSQL access

  • preserve the existing Turso schema with a non-destructive baseline and guarded setup scripts

  • fail closed when production database credentials are incomplete

  • make inline prose spacing explicit for Astro 7 JSX whitespace handling

  • remove obsolete remote build flags and add compliance and database regression coverage

build: upgrade Astro stack and preserve PWA

July 22, 2026asce1062View on GitHubc8493db
Commit details
Upgrade Astro, lint, formatting, and supporting packages while adapting changed parser, OpenGraph, Tailwind, and TypeScript contracts.

  • replace the Astro-6-only PWA adapter with a tested Astro 7 lifecycle bridge

  • preserve manifest, offline fallback, precache, runtime-cache, and Netlify service-worker output

  • remove obsolete CollapsibleNav and MagicalText compatibility code

  • regenerate the lockfile without legacy peer overrides

fix: honor manual theme toggle for dark Shiki code blocks

July 7, 2026asce1062View on GitHub8f130e0
Commit details
Dark code-block colors were gated behind @media (prefers-color-scheme:
dark), so they only applied when the OS preference also happened to be
dark. Since theme is driven entirely by the html[data-theme] attribute,
this left code blocks stuck on the light Shiki theme whenever the site
was toggled dark but the OS/browser preference was light.

Merge branch 'update/colors'

July 7, 2026asce1062View on GitHub96aa990
Commit details
  • update/colors:

upate site greeting font
feat(theme): add observatory flavor, rework color system to dark-first

upate site greeting font

July 7, 2026asce1062View on GitHub9a18548

feat(theme): add observatory flavor, rework color system to dark-first

July 7, 2026asce1062View on GitHub5116b65
Commit details
theme.css:
  • Flip default @theme from light-first to dark-first; light now lives

under the [data-theme="light"] override
  • Add "observatory" flavor (deep-space indigo, nebula violet, gold

starlight) for both dark and light
  • Decouple --color-link from --color-warning to --color-primary

  • Add --text-display/--font-display (uni0553) for hero/greeting moments

  • Reduce heading font-weight 800 -> 700; retune flavor hue/chroma values


Consumers (design-tokens.ts is a manual snapshot for /palette - runtime
colors come from CSS vars, but the doc copy had drifted):
  • Resync colorValues, hexColorValues, and all 7 existing themeFlavors

entries in design-tokens.ts to the new theme.css values
  • Wire "observatory" into flavorManager, transitionRegistry (ripple

transition), musicPlayer flavor mapping, asciiVisualizer theme,
burn404palettes fire stops, and navBrand command-palette targets
  • Extend transitionRegistry/burn404palettes/musicPlayer/navBrandCommands

tests for observatory coverage

Verification: npm run check (0 errors), npm run lint (clean), vitest
524/524 passing across 30 files.

Merge branch 'fix/8biticon'

June 26, 2026asce1062View on GitHub95d9b65
Commit details
  • fix/8biticon:

fix: remove redundant DOMContentLoaded guards causing double init on page load

fix: remove redundant DOMContentLoaded guards causing double init on page load

June 26, 2026asce1062View on GitHubba093b1
Commit details
astro:page-load fires on initial load and every subsequent soft navigation,
making the readyState/DOMContentLoaded guard pattern redundant. Having both
caused init functions to run twice on first visit, stacking duplicate event
listeners on buttons, scroll handlers, and keyboard shortcuts.

Affected: AvatarGenerator, BackToTop, CollapsibleNav, FloatingSearch,
SearchField, TerminalModal, SocialShareButtons, Layout (scroll progress),
resume (PDF viewer), terminalModal.ts

Merge branch 'updates'

June 23, 2026asce1062View on GitHuba9d8935
Commit details
  • updates:

refactor(music-player): revert ASCII visualizer to linear frequency mapping
fix(music-player): close partially-created AudioContext on connect() failure
perf(music-player): harden and optimize ASCII visualizer

refactor(music-player): revert ASCII visualizer to linear frequency mapping

June 23, 2026asce1062View on GitHubf981092
Commit details
Replace log-spaced bin ranges with simple equal-width binsPerCol averaging.
Log mapping gave low frequencies extra visual space, making the display
bass-forward. Linear slicing keeps each column balanced across the spectrum.

fix(music-player): close partially-created AudioContext on connect() failure

June 23, 2026asce1062View on GitHubf8b60f3

perf(music-player): harden and optimize ASCII visualizer

June 23, 2026asce1062View on GitHub8515bc2
Commit details
  • Fix isPlaying tracking: set flag before reduced-motion early return so

motion listener correctly resumes active bars vs idle diamonds
  • Harden motion listener: explicitly stop the opposite loop when toggling

reduced-motion on/off to prevent double-running RAF and setInterval
  • Guard RAF startup: skip active mode if AudioContext setup failed,

avoiding a wasted RAF loop that would just re-render idle
  • Fix AudioContext resume: use void + .catch(() => {}) to silence

unhandled rejection noise if browser refuses resume
  • Log-spaced bin ranges: precompute {start,end} per column once in

connect() instead of calling Math.exp() every frame
  • Include bin 0 in column 0 for fuller sub-bass representation

  • Peak decay: previousHeights buffer smooths bar falloff (VU-meter feel)

  • DOM write cache: skip textContent/color/shadow writes when unchanged

  • Immediate idle repaint on setFlavor() when not in active mode

  • Reduced-motion live listener: react to OS setting without page reload

  • Reset isPlaying and logBinRanges in teardown() for full state hygiene

Merge branch 'updates'

June 22, 2026asce1062View on GitHubf80d408
Commit details
  • updates:

feat(music-player): add real-time ASCII audio visualizer

feat(music-player): add real-time ASCII audio visualizer

June 22, 2026asce1062View on GitHub7b3de1a
Commit details
Replaces the static AsciiWidget placeholder in the music player's right
panel with a live FFT-driven ASCII visualizer. Shows a scrolling diamond
idle pattern when paused and 32×12 frequency bars during playback.

  • New AsciiVisualizerController class (src/lib/asciiVisualizer.ts):

pre-allocated span grid, Web Audio API AnalyserNode (fftSize=256),
lazy AudioContext init on first play, setInterval idle loop,
requestAnimationFrame active loop, prefers-reduced-motion support
  • 8 flavor themes all derived from --player-ink / --player-bg CSS vars

so colors automatically track the active site flavor; per-flavor
personality via ramp width and glow radius; REDLINE uses a power-curve
ramp for extra punch at peaks
  • Wired into musicPlayerModal.ts: connect+setPlaying on audio events,

setFlavor on theme change, teardown on cleanup/close
  • CSS: removed .ascii-widget-* rules, added .music-ascii-visualizer

with bottom-alignment so the grid sits flush with the album art

Merge branch 'updates'

June 22, 2026asce1062View on GitHubb50f38c
Commit details
  • updates:

fix(a11y): resolve aria-hidden violations on focus in haunted and music player

fix(a11y): resolve aria-hidden violations on focus in haunted and music player

June 22, 2026asce1062View on GitHubfa29abd
Commit details
Use parseCSSColor to convert oklch computed values to rgb channels before
building rgba() box-shadow strings in haunted/controller.ts, preventing
invalid keyframe values when --color-primary resolves to a modern color space.

Return focus to the music launcher trigger before hiding the music terminal
dialog (close and minimize actions) so aria-hidden is never set while a
descendant button retains focus.

Merge branch 'update/music-player'

June 22, 2026asce1062View on GitHub8b7a366
Commit details
  • update/music-player:

fix(music-player): retain active playback queue when browsing albums

fix(music-player): retain active playback queue when browsing albums

June 22, 2026asce1062View on GitHubf580d18
Commit details
Music controls (next/prev/auto-advance) were routing to the browsed
album instead of the playing album, because renderTracks() replaced
playbackState.playlistTracks on every album open.

Separate browsing state (browsingPlaylist/browsingTracks) from playback
state. The active queue is only replaced in playTrackFromBrowse() when
the user explicitly plays a track, leaving controls anchored to the
playing album while the user browses freely.

Merge branch 'update/effects'

June 18, 2026asce1062View on GitHub9d171da
Commit details
  • update/effects:

feat(effects): add creatureColors prop to Haunted with round-robin distribution
feat(effects): extend components with arbitrary tag and attribute forwarding
refactor(effects): centralize effect registries in Layout.astro
fix(effects): re-resolve theme CSS vars on flavor-change

feat(effects): add creatureColors prop to Haunted with round-robin distribution

June 18, 2026asce1062View on GitHubb826c9d
Commit details
Accept an array of CSS colors on the Haunted component/data-attribute API.
Colors are distributed across the N creatures in round-robin order. Defaults
to --color-primary from the active theme and re-resolves on flavor-change.
Has no effect on creatures with hardcoded SVG fills (ghost, ghost-alt).

feat(effects): extend components with arbitrary tag and attribute forwarding

June 18, 2026asce1062View on GitHub863a69f
Commit details
  • MagicalText and Haunted now accept a tag prop (sup, i, b, em, strong, a, etc.)

and forward any unknown HTML attributes (role, tabindex, aria-*, data-*) to the
root element via ...rest spread, enabling use as interactive elements
  • Fix multi-path SVG color tracking: wrap sibling paths in <g data-sparkle-path>

so the single querySelector hit cascades fill to all children (fixes music-note
showing only the note head colored)
  • Add north-star, music-note, and cassette adornments to the shared registry and

Haunted creature map
  • Import magical-text.css and haunted.css via global.css so data-attribute

standalone usage works without requiring a component import on the page
  • Apply haunted (music-note creature) and magical-text (cassette adornment) effects

to the music player launcher on the home page

refactor(effects): centralize effect registries in Layout.astro

June 17, 2026asce1062View on GitHuba053b99
Commit details
Move textEffectRegistry, magicalTextRegistry, and hauntedRegistry imports
from individual components (Header, Sidebar, MagicalText, Haunted) into
Layout.astro so all three effects work declaratively via data attributes
on any page without requiring per-page or per-component script imports.

fix(effects): re-resolve theme CSS vars on flavor-change

June 17, 2026asce1062View on GitHub79d2450
Commit details
MagicalText and Haunted were resolving --color-primary / --color-accent
once at mount time, so colors stayed stale when the user cycled flavors.
Both controllers now listen for the flavor-change document event and
re-resolve their defaults live; listeners are torn down with the rest of
cleanup. User-supplied colors and glow overrides are left untouched.

Merge branch 'update/text-effects'

June 17, 2026asce1062View on GitHub61bb57b
Commit details
  • update/text-effects:

feat(textEffects): add standalone typewriter effect
feat(textEffects): add standalone glitch effect
refactor(textEffects): rename GlitchEffectOptions and glitchOptions to GlitchLockOnEffectOptions and glitchLockOnOptions
feat(textEffects): overhaul standalone effects with restore, delay, items options

feat(textEffects): add standalone typewriter effect

June 17, 2026asce1062View on GitHub0e004e6
Commit details
Implements a full typewriter renderer with:
  • Character-by-character typing with configurable delay and stutter

  • Cursor blinking via visibility toggling (no layout shift in grid/flex/centered contexts)

  • Cycle mode: loops through pipe-separated strings, backspacing between items

  • Lead-in delay: blinks cursor before typing starts (matches reference behavior)

  • Existing text + cycle: types element content first, then cycles through items

  • Empty element support: cycle plays even when element has no prior text

  • DOM span wrapper with all: inherit; display: inline to preserve element styles

  • WeakMap-based orphaned blink cleanup on retrigger

feat(textEffects): add standalone glitch effect

June 16, 2026asce1062View on GitHubfe86d9e
Commit details
Two-phase renderer: Phase 1 progressively locks in characters from fully
randomised noise (left→right or right→left with reverse); Phase 2 runs a
persistent post-settle shimmer that randomly corrupts and restores a single
character on a random timer until cancel() is called.

Options: charset, items (overrides charset), reverse, delayMs, count (extra
noise frames before reveal), shimmerIntervalMs, shimmer (disable post-settle
loop). All options are wired through the full declarative pipeline —
dataset attributes, config parser, triggers, transition — and exposed via
the registry under data-text-effect="glitch".

refactor(textEffects): rename GlitchEffectOptions and glitchOptions to GlitchLockOnEffectOptions and glitchLockOnOptions

June 15, 2026asce1062View on GitHub8a99892
Commit details
Disambiguates the glitch-lock-on effect's types and option keys from the
upcoming standalone glitch effect, which would otherwise collide on the
same names across types, config, transition, triggers, and registry.

feat(textEffects): overhaul standalone effects with restore, delay, items options

June 15, 2026asce1062View on GitHub445acef
Commit details
  • Rename glitch → corruption (file, types, constants, all references); glitch-lock-on untouched

  • Add corruption: count, delayMs, items, restore options; rename data attr frames→count

  • Add uncensor: delayMs option with letter-by-letter reveal using nonSpaceIndices

  • Add scramble: delayMs, count, items, restore; two-phase animated unscramble restore

  • Add slow-reveal: delayMs, items; fix default charset (was signal artifacts, now #!@$%^&*()-_+=)

  • Add shuffle: delayMs, restore; two-phase restore from last shuffled state

  • Add censor letter-spacing note to JSDoc

  • Generalize censorSkipRestore → skipFinalRestore covering all restore-capable effects

  • Fix corruption/scramble/shuffle restore=false not persisting (coordinator reset bug)

  • Fix random-time test timing (advance 21s not 18s to clear typing + cursor blink)

Merge branch 'update/text-effects'

June 15, 2026asce1062View on GitHub5fac548
Commit details
  • update/text-effects:

feat(effects): wire MagicalText and Haunted defaults to active theme tokens

feat(effects): wire MagicalText and Haunted defaults to active theme tokens

June 15, 2026asce1062View on GitHub7ccfc03
Commit details
Add resolveThemeCSSVar() to shared/colorUtils that resolves a CSS custom
property to a concrete rgb() string via a short-lived probe element, making
it compatible with the existing parseCSSColor pipeline.

MagicalText now defaults gradient colors to --color-primary and --color-accent
instead of the static 'darkorange'/'purple' pair. Haunted glow now defaults to
--color-primary instead of hardcoded red. Both remain fully overridable via
their existing options interfaces.

Merge branch 'update/text-effects'

June 13, 2026asce1062View on GitHub2346fd5
Commit details
  • update/text-effects:

feat(haunted): add Haunted container with creature fly-out and glow effects
feat(magicalText): add hsl/oklch color support; tighten barrel exports
feat(magicalText): add MagicalText gradient-sweep text with sparkle adornments
feat(textEffects): add 6 standalone rare effects (glitch, censor, uncensor, scramble, slow-reveal, shuffle)
feat(textEffects): improve and rename text effect system
refactor(textEffects): split terminalTextEffect.ts into focused modules

feat(haunted): add Haunted container with creature fly-out and glow effects

June 13, 2026asce1062View on GitHubd66a2c6
Commit details
Implements the Haunted component — a container that emits flying SVG
creatures and pulses with a box-shadow glow on hover.

Refactor: extract shared/colorUtils and shared/adornments from
magicalText/ so both MagicalText and Haunted draw from a single source
of truth. colorUtils.ts re-export shim removed (no consumers).

New files:
src/lib/shared/colorUtils.ts — RGBColor, color parsers, multiColorFade
src/lib/shared/adornments.ts — AdornmentKind, AnimationType, ADORNMENTS, SVG strings
src/lib/haunted/types.ts — CreatureKind, HauntedOptions
src/lib/haunted/creatures.ts — CREATURES registry (SVGs from shared/adornments)
src/lib/haunted/controller.ts — bindHaunted() via Web Animations API
src/lib/haunted/index.ts — barrel export
src/styles/components/haunted.css — base positioning styles
src/scripts/hauntedRegistry.ts — declarative data-attribute registry
src/components/Haunted.astro — typed Astro component

feat(magicalText): add hsl/oklch color support; tighten barrel exports

June 13, 2026asce1062View on GitHub74af02b
Commit details
colorUtils.ts:
  • Add hslStringToRgb — handles hsl()/hsla(), comma and space syntax, deg suffix

  • Add oklchStringToRgb — full OKLCH→OKLab→linear sRGB→gamma pipeline (Ottosson 2020)

  • Add JSDoc to hexToRgb, rgbStringToRgb, hue2rgb

  • Both parsers sit before the canvas fallback, so they work without a DOM


index.ts: remove ADORNMENTS, parseCSSColor, multiColorFade from barrel
(internal utilities consumed directly by controller.ts; not part of public API)

magicalTextRegistry.ts, MagicalText.astro: import through barrel instead of sub-modules
(fixes knip "Unused files" report on index.ts)

feat(magicalText): add MagicalText gradient-sweep text with sparkle adornments

June 13, 2026asce1062View on GitHub3e41ee8
Commit details
Provides two integration paths that share the same core engine:

Astro component:
<MagicalText text="Hello" colors={['hotpink','purple']} adornment="heart" />

Declarative data-attribute path (works on any element):
<span data-magical-text data-magical-text-colors="hotpink,purple">Hello</span>

Core (src/lib/magicalText/):
  • colorUtils.ts — parseCSSColor (hex/rgb/named) + multiColorFade (200-step table)

  • adornments.ts — four inline SVG adornments with authoring docs for adding more

  • controller.ts — bindMagicalText() mounts gradient + rAF sparkle lifecycle

  • types.ts — AdornmentKind, AnimationType, MagicalTextOptions


CSS (src/styles/components/magical-text.css):
  • Gradient sweep via @keyframes (background-position 0% → -200%)

  • Sparkle scale+rotate (@keyframes magical-text-sparkle)

  • Scale-only variant (@keyframes magical-text-scale)

  • prefers-reduced-motion: gradient freezes, adornments hidden


Adornments: star, heart (gradient color-tracked via rAF); ghost, ghost-alt (static fills)
Color tracking SparkleAnimator approach:
multiColorFade(colors, 200) builds a 200-step table; each frame maps sparkle X
position + current gradient offset to the matching table entry.

Astro navigation: astro:before-swap cancels all rAF loops; astro:page-load re-inits.

feat(textEffects): add 6 standalone rare effects (glitch, censor, uncensor, scramble, slow-reveal, shuffle)

June 12, 2026asce1062View on GitHub2e6b66d
Commit details
Adds 6 new role: "standalone" effects to the rare family, each with
typed option bags, HTML data attributes, and a dedicated renderer:

  • glitch — per-frame intensity-based burst corruption, restores

  • censor — fills chars L-to-R with block char, restores by default

  • uncensor — starts fully censored, reveals chars L-to-R

  • scramble — accumulates one random noise char per tick, restores

  • slow-reveal — per-char slot-machine cycles before locking in L-to-R

  • shuffle — Fisher-Yates anagram frame, restores


Architecture changes:
  • resolveGlitchCharsetStr moved to utils.ts as a shared export;

glitchLockOn.ts now imports it rather than duplicating
  • playTextEffect mode resolution extended: role: "standalone"

effects now correctly enter "standalone" mode (play + restore)
  • glitch-lock-on data attributes renamed from glitch-* to

glitch-lock-* to free the glitch-* namespace for the new
standalone glitch burst effect
  • All 6 new effects wired through config, transition, triggers,

and textEffectRegistry option bag passthrough

feat(textEffects): improve and rename text effect system

June 12, 2026asce1062View on GitHubea09077
Commit details
Drops the Terminal prefix throughout (TerminalTextEffect* → TextEffect*,
TERMINAL_TEXT_EFFECT* → TEXT_EFFECT*, terminalTextEffect.ts → textEffect.ts)
to reflect that these are generic text effects, not terminal-specific.

Also adds per-effect customisation options to typing, backspace, glitch-lock-on,
and signal-loss renderers — exposed via both the JS API and zero-JS data attributes:

Typing / backspace:
data-text-effect-typing-cursor-char, -cursor-blink-ms, -end-blink-count,
-lead-in-ms, -stutter-chance, -stutter-max-ms

Glitch-lock-on:
data-text-effect-glitch-charset ("blocks" | "letters" | "binary" | custom),
-reverse (right-to-left lock-in), -frames, -intensity

Signal-loss:
data-text-effect-signal-dropout-char, -blackout-ms, -false-recovery

All defaults preserved; zero breaking changes. 0 type errors, 506/506 tests passing.

refactor(textEffects): split terminalTextEffect.ts into focused modules

June 12, 2026asce1062View on GitHub8c1fecc
Commit details
1368-line monolith split into types, constants, utils, config,
activeEffects, transition, triggers, and effects/* per the module
dependency DAG. Barrel re-exports preserve the full public API.
No logic changes.

Merge branch 'update/404-burn-page'

June 11, 2026asce1062View on GitHub624d1b9
Commit details
  • update/404-burn-page:

fix(burn404): skip resize on height-only changes (mobile chrome toggle)

fix(burn404): skip resize on height-only changes (mobile chrome toggle)

June 11, 2026asce1062View on GitHub1a4ee22
Commit details
Root cause: _canvas.width = X always clears the canvas bitmap, even when
the value changes only slightly. Mobile browser chrome show/hide triggers a
resize event that changes only window.innerHeight (width stays constant),
which caused both canvases to clear and restart the fire animation on every
scroll-then-stop cycle.

Fix: return early in resizeCanvas / resizeTorchCanvas when newFireW ===
_fireW (width unchanged). Height-only events are chrome toggles — the
canvas keeps its current size, anchored at bottom: 0 / top: 0, with no
bitmap disruption. Width changes (device rotation, desktop resize) still
trigger a full update with buffer state copied across dimensions.

Merge branch 'update/404-burn-page'

June 11, 2026asce1062View on GitHub6ab5a4d
Commit details
  • update/404-burn-page:

fix(burn404): preserve fire buffer on resize and extend detection overflow

fix(burn404): preserve fire buffer on resize and extend detection overflow

June 11, 2026asce1062View on GitHub9102a86
Commit details
Both canvases were reinitializing their fire buffers completely on every
resize event. Mobile browser chrome show/hide changes window.innerHeight
by ~60px during scroll, which always produced a different cell count and
bypassed the existing dimension guard, causing the fire to visually restart.

Buffer preservation: copy existing cells into the new buffer on resize,
clamping to the overlapping region. The source row is always restored at
full intensity for the bottom strip. Burning torch pixels outside the new
bounds are dropped; the rest are retained.

Detection overflow: increase DETECT_OVERFLOW from 8px to 64px and extend
all three visibility checks to canvasH + DETECT_OVERFLOW. On screens where
the 404 page is marginally taller than the viewport (e.g. 2560x1440), the
final content section was past canvasH and got silently skipped. The extra
64px means content up to one screen-cell below the fold is still detected
and mapped to the last fire row.

Merge branch 'update/404-burn-page'

June 11, 2026asce1062View on GitHub758b20f
Commit details
  • update/404-burn-page:

fix(burn404): debounce resize and skip reinit when dimensions unchanged
fix(burn404): fill all text glyphs and re-detect on sidebar toggle

fix(burn404): debounce resize and skip reinit when dimensions unchanged

June 11, 2026asce1062View on GitHub9af824c
Commit details
On mobile, the browser chrome slides in/out during scroll, firing resize
events that called resizeCanvas() and reset _buf via initFire() — making
the bottom fire strip visually restart after every scroll stop.

Two-layer fix:
1. resizeCanvas() now checks whether the cell grid (fireW × fireH) actually
changed before reinitialising _buf. If only the CSS display size changed
(browser chrome height delta within a cell boundary), the running fire
state is preserved.
2. onResize() debounces the resize handler by 250 ms so the buffer only
reinitialises once the user has fully stopped resizing, not mid-scroll.

fix(burn404): fill all text glyphs and re-detect on sidebar toggle

June 11, 2026asce1062View on GitHub781a8ad
Commit details
Two fixes:

1. Text visible through flames on large screens — caused by strokeText-only
detection for large text (≥24 px): only outline pixels were flammable so
character interiors never entered _burningPixels and showed through the
fire overlay. Removed the useStroke distinction; all text now uses fillText
so every glyph pixel is marked flammable and fully covered when burnt.

2. Sidebar collapse/expand shifts page content — main content re-centres
when the sidebar collapses, making previously detected flammable coords
stale. Added _sidebarObserver (MutationObserver on document.documentElement
attributeFilter: data-sidebar-collapsed) that triggers redetectAfterChange()
after a 300 ms delay to let the CSS transition complete before re-sampling
element positions. Started in enableTorch(), stopped in disableTorch().

Merge branch 'update/404-burn-page'

June 11, 2026asce1062View on GitHubfc9fb90
Commit details
  • update/404-burn-page:

fix(burn404): accurate glyph detection, mutation re-scan, and deferred init

fix(burn404): accurate glyph detection, mutation re-scan, and deferred init

June 11, 2026asce1062View on GitHub8d22a48
Commit details
Three flammable-detection bugs fixed:

1. Descenders ('y', 'g') not igniting — baseline was r.top + r.height * 0.8
which ignores line-gap; corrected to r.top + (r.height - fontSize)/2 +
fontSize * 0.8. Extended detection canvas by DETECT_OVERFLOW (8 px) and
clamped fy so viewport-bottom descenders map to the last fire row instead
of being silently discarded.

2. Neofetch widget ASCII art randomization — added MutationObserver on
[data-page-404] (childList + subtree, 150 ms debounce). On DOM change:
clears _burningPixels and re-runs detection after document.fonts.ready so
new art positions replace stale flammable coords.

3. Post-navigation / reload timing — initTorch() now defers
detectFlammablePixels() behind document.fonts.ready + 50 ms timeout.
This prevents Range.getClientRects() from sampling intermediate positions
during Astro view transitions and back-navigation.

Merge branch 'feature/404-burn-page'

June 11, 2026asce1062View on GitHub6d705f3
Commit details
  • feature/404-burn-page:

feat(burn404): icon-fire, default-on, granular ignition, and privacy updates
feat(burn404): theme-aware palettes and graceful extinguish animation
fix(burn404torch): canvas pixel-scan detection + scroll extinguishing
feat(burn404): add full-viewport torch with flammable text detection
chore(burn404): cap source intensity at 33 to reduce white at flame base
fix(burn404): remove stacking context override that broke sidebar layout
fix(burn404): cancel RAF and clear active attr on non-404 navigation
feat(burn404): add data-page-404 sentinel and reduced-motion gradient to 404 page
docs(privacy): disclose flourish-burn404 localStorage key (12 total)
feat(burn404): wire burn404 script into Sidebar flourish block
feat(burn404): add canvas positioning CSS and reduced-motion fallback
feat(burn404): implement Doom-fire engine and lifecycle manager
feat(prefs): add burn404 localStorage key

feat(burn404): icon-fire, default-on, granular ignition, and privacy updates

June 11, 2026asce1062View on GitHub7a442f1
Commit details
  • Use icon-fire for the Burn 404 sidebar toggle (was icon-stars)

  • Flip default: burn404 is ON by default; storage key "0" = disabled,

absent = enabled. Updated isActive() in both burn404.ts and
burn404torch.ts, toggle handler, prefs.ts comment, privacy.astro,
and dnt-policy.txt to reflect the new semantics.
  • Replace bounding-rect flammable detection with glyph-pixel accuracy:

large text (≥24px) uses strokeText so fire traces letter outlines;
small text uses fillText; short nodes (≤200 chars) render
character-by-character via Range for ASCII art glyph granularity;
long nodes fall back to line-level for performance.
  • Split ignition radius from visual fire radius: TORCH_IGNITION_RADIUS=2

(tight contact zone) vs TORCH_FIRE_RADIUS=8 (visual flame spread),
so fire only permanently lights pixels directly under the torch tip.
  • icomoon font updated with icon-fire glyph.

feat(burn404): theme-aware palettes and graceful extinguish animation

June 11, 2026asce1062View on GitHub0394745
Commit details
  • Add burn404palettes.ts with 16 color-stop presets (8 dark + 8 light)

covering all flavors: dark, crt-green, amber, synthwave, dos, void, ice,
redline. First two stops are always transparent to prevent cold-cell
overlay on light backgrounds.
  • buildPalette() interpolates stops via canvas linear gradient into the

37-step RGBA palette used by the Doom-fire renderer.
  • getActiveColorStops() reads data-theme / data-flavor from the DOM so

the fire instantly reflects the active theme and flavor in both modes.
  • rebuildPalette() wired to set-theme and flavor-change events so color

updates are live without a page reload.
  • disable() now sets _extinguishing=true when the RAF loop is running;

updateFire() decays the source row naturally until the buffer hits zero,
then cleans up — producing a gradual burn-out instead of a hard cut.
  • Torch mirrors the same extinguish pattern via _torchExtinguishing and

listens to BURN404_PREF_CHANGE so both canvases fade out together.
  • Tests cover FIRE_COLOR_STOPS structure invariants and SSR fallback paths.

fix(burn404torch): canvas pixel-scan detection + scroll extinguishing

June 11, 2026asce1062View on GitHubd1692d9
Commit details
Detection now mirrors the 404-fire-page reference approach:
  • Creates an offscreen canvas at viewport resolution (fireW*pixelSize × fireH*pixelSize)

  • Paints all visible leaf elements (no child elements) by bounding rect, covering

icons <i>, <hr>, <pre> ASCII art, inline spans, links, and any other non-container
visible content — not just text nodes
  • Also overlays text node Range.getClientRects() rects for per-line accuracy inside

compound elements
  • Pixel-scans the result: any white pixel → flammable fire-buffer cell


Scroll extinguishing:
  • On scroll: _burningPixels cleared immediately (fire decays naturally)

  • 200ms after scroll stops: detectFlammablePixels() re-runs so coordinates stay

aligned with the new viewport position
  • _scrollTimer cleared in disableTorch() to prevent ghost re-detection

feat(burn404): add full-viewport torch with flammable text detection

June 11, 2026asce1062View on GitHub928f2b9
Commit details
Adds a second fire layer (#burn404-torch) that covers the full viewport.
Mouse/touch position injects Doom-fire at the cursor with a circular radius
and quadratic falloff. Text content on the 404 page is detected as
"flammable" via Range.getClientRects() — cells touched by the cursor are
permanently ignited and held at max intensity each frame.

  • burn404torch.ts: initTorchFire, updateTorchFire (9 tests), detectFlammablePixels

(DOM TreeWalker + Range rects → fire-buffer coordinates), full canvas
lifecycle with AbortController cleanup
  • burn404.css: #burn404-torch full-viewport styles; cursor:none on main

when burn is active
  • Sidebar.astro: import burn404torch alongside burn404

  • Both layers share the flourish-burn404 preference key and astro lifecycle

chore(burn404): cap source intensity at 33 to reduce white at flame base

June 11, 2026asce1062View on GitHubc95034f
Commit details
MAX_INTENSITY (36) is pure white — setting the source row to SOURCE_INTENSITY
(33, palette index [255, 247, 127]) keeps the base amber-yellow without the
stark white strip.

fix(burn404): remove stacking context override that broke sidebar layout

June 11, 2026asce1062View on GitHub008e69a
Commit details
The body > :not(#burn404-canvas) { position: relative; z-index: 2 } rule
had specificity 1,1,1, overriding Tailwind's .fixed class (0,1,0) and
un-fixing the sidebar from the viewport. The sidebar fell into normal flow,
pushing all content down and breaking the layout.

Removed the rule. The sidebar's own z-40 (z-index: 40) keeps it above the
canvas (z-index: 1) naturally. Content in normal flow sits below the fixed
canvas — fire overlays the content at the bottom, sidebar stays above it.

fix(burn404): cancel RAF and clear active attr on non-404 navigation

June 10, 2026asce1062View on GitHube808d3f
Commit details
When pref is enabled and the user navigates away from the 404 page,
enable() was leaving the RAF loop running on a detached canvas and
setting data-burn404-active on non-404 pages, which triggered the
z-index stacking context CSS site-wide with no canvas present.

Fix: call stopAnimation() first in enable(), then guard the attribute
set and canvas creation behind isOn404Page() — attribute is now only
set when fire is actually rendered, and the RAF is always cancelled
before re-evaluating whether to start it.

feat(burn404): add data-page-404 sentinel and reduced-motion gradient to 404 page

June 10, 2026asce1062View on GitHub77029a2

docs(privacy): disclose flourish-burn404 localStorage key (12 total)

June 10, 2026asce1062View on GitHubc90857b

feat(burn404): wire burn404 script into Sidebar flourish block

June 10, 2026asce1062View on GitHub2be3b77

feat(burn404): add canvas positioning CSS and reduced-motion fallback

June 10, 2026asce1062View on GitHubf703ca5

feat(burn404): implement Doom-fire engine and lifecycle manager

June 10, 2026asce1062View on GitHubb0fa22f
Commit details
  • Export PALETTE (37-step Doom-fire color ramp) and MAX_INTENSITY = 36

  • initFire(): initialise flat buffer with hot bottom row

  • updateFire(): classic Doom-fire PSX algorithm (decay + drift)

  • Canvas lifecycle: build/resize/remove with resize listener cleanup

  • ~30 fps RAF loop using ImageData for per-pixel efficiency

  • isOn404Page(): data-page-404 sentinel gates canvas creation

  • Respects prefers-reduced-motion (no RAF loop when set)

  • Lifecycle mirrors starsBackground.ts / matrixBackground.ts patterns

  • Guard lifecycle side-effects behind typeof document !== "undefined" for test compatibility

feat(prefs): add burn404 localStorage key

June 10, 2026asce1062View on GitHubf2eaf64

Merge branch 'fix/music-active-row-text-color'

June 8, 2026asce1062View on GitHubb5ba19e
Commit details
  • fix/music-active-row-text-color:

fix(music): preserve active row inverse text color

fix(music): preserve active row inverse text color

June 8, 2026asce1062View on GitHub6cc6d45
Commit details
Ensure active and playing music rows keep their inverse text color even
when nested text elements match the global typography color rule.

- apply player inverse color to text descendants inside active and playing rows

Merge branch 'fix/music-scrollbar-resize-overlap'

June 8, 2026asce1062View on GitHubc1346d2
Commit details
  • fix/music-scrollbar-resize-overlap:

fix(music): reduce right resize handle overlap

fix(music): reduce right resize handle overlap

June 8, 2026asce1062View on GitHub0f325df
Commit details
Narrow the music dialog right-edge resize handle so it remains available
without blocking normal scrollbar interaction in the scrollable body.

- keep the left resize edge at the larger hit target
- reduce the right resize hit area to avoid competing with the scrollbar

Merge branch 'feat/theme-scrollbars'

June 8, 2026asce1062View on GitHub183ba0d
Commit details
  • feat/theme-scrollbars:

feat(theme): add site-wide terminal scrollbars

feat(theme): add site-wide terminal scrollbars

June 8, 2026asce1062View on GitHubdb06902
Commit details
Add a global themed scrollbar skin that matches the terminal visual
system and inherits from the active theme flavor.

- add reusable scrollbar color tokens in theme.css
- style WebKit scrollbars with square thumbs, transparent tracks, and primary-colored borders
- keep Firefox scrollbar colors scoped to its supported properties
- remove component-level scrollbar overrides so global styling applies consistently
- scope music player scrollbar tokens to the player theme so local flavor changes do not affect the rest of the site

Merge branch 'fix/misc'

June 8, 2026asce1062View on GitHubc2781a3
Commit details
  • fix/misc:

build(astro): update Astro 6.4 and fix music player layout

build(astro): update Astro 6.4 and fix music player layout

June 8, 2026asce1062View on GitHub184ab0d
Commit details
Update Astro and related packages to the 6.4 line, including the MDX,
Netlify, sitemap, Shiki, lint, and test toolchain bumps captured in the
lockfile.

- move Markdown remark/rehype configuration into unified() for Astro 6.4
- add @astrojs/markdown-remark as a direct dependency for the config import
- remove stale vite-plugin-pwa from Knip ignored dependencies
- let the expanded music dialog scale on large desktop viewports
- fix playlist expansion so the panel grows with long track lists
- keep music controls and active playlist headers sticky while scrolling
- place terminal/music dock chips correctly below the desktop sidebar breakpoint

Merge branch 'fix/misc'

May 22, 2026asce1062View on GitHub35aada8
Commit details
  • fix/misc:

fix(site): align redirects and footnote navigation

fix(site): align redirects and footnote navigation

May 22, 2026asce1062View on GitHub3252d99
Commit details
Update PWA navigation fallback handling so Netlify vanity redirects for /why and /licensing are
allowed through to the network, and remove the obsolete /resume redirect now that /resume is a
real page.

Improve blog footnote back-navigation by keeping the TOC hidden during return jumps and adding
extra scroll margin for inline footnote reference targets so the source line stays visible.

Stabilize admin auth missing-env tests by explicitly stubbing ADMIN_TOKEN_HASH as undefined
instead of restoring the real shell environment.

Verification:

- npx vitest run src/lib/api/tests/admin-auth.test.ts
- npm test
- npm run check
- npm run lint

Merge branch 'docs-admin-argon2-auth-post'

May 22, 2026asce1062View on GitHubc483b01
Commit details
  • docs-admin-argon2-auth-post:

blog(auth): add Argon2id security blog post pair

blog(auth): add Argon2id security blog post pair

May 22, 2026asce1062View on GitHub7a59df7
Commit details
Add two linked security posts around the guestbook admin auth migration:

- add a standalone Argon2id/password-security explainer covering fast hashes,
memory hardness, storage format, tuning, and practical defaults
- add the guestbook migration write-up covering raw token removal,
Argon2-backed verification, signed sessions, query-token removal,
deployment notes, and test coverage
- add blog artwork for both posts
- normalize the admin token hash helper script file mode

Merge branch 'feature/sidebar-keyboard-shortcut'

May 18, 2026asce1062View on GitHubb021b94
Commit details
  • feature/sidebar-keyboard-shortcut:

feat(sidebar): add global collapse shortcut

feat(sidebar): add global collapse shortcut

May 18, 2026asce1062View on GitHub281c547
Commit details
Add a global Cmd+. / Ctrl+. shortcut for toggling the desktop sidebar
through the existing sidebar collapse state path.

- expose tested shortcut matching and platform-aware shortcut labels
- wire the shortcut into the sidebar collapse controller
- update the collapse tab accessible label, title, and aria-keyshortcuts
- keep shortcut handling inactive inside editable controls

Merge branch 'feature/admin-token-generator'

May 18, 2026asce1062View on GitHub31655b5
Commit details
  • feature/admin-token-generator:

feat(admin): add interactive token generation

feat(admin): add interactive token generation

May 18, 2026asce1062View on GitHubdebbee0
Commit details
Extend the admin token hash helper so operators can generate a raw token and Argon2id hash in one command.

- add --generate mode for explicit raw token creation
- let blank interactive input generate a token and print storage guidance
- document npm-based generation and local dotenv escaping
- cover generated-token paths in the script tests

Verification:
  • npm test -- src/lib/api/tests/hash-admin-token-script.test.ts

Merge branch 'feature/admin-token-hash-auth'

May 18, 2026asce1062View on GitHub7cb4357
Commit details
  • feature/admin-token-hash-auth:

[feature] migrate: admin guestbook auth to argon2 hash

[feature] migrate: admin guestbook auth to argon2 hash

May 18, 2026asce1062View on GitHub7727409
Commit details
Replace raw ADMIN_TOKEN authentication with ADMIN_TOKEN_HASH-backed
Argon2 verification for the admin guestbook flow.

- verify raw bearer/login tokens with @node-rs/argon2
- remove query-string token upgrade support
- replace raw-token cookies with signed admin session cookies
- add a production-safe admin token hash helper script
- document .env variable escaping and deployment migration steps
- update auth and middleware tests for valid, invalid, missing, and query-token cases

Merge branch 'feature/music-page'

May 16, 2026asce1062View on GitHub2e3a982
Commit details
  • feature/music-page:

feat(music): add floating Navidrome player

feat(music): add floating Navidrome player

May 16, 2026asce1062View on GitHub7c5b2d5
Commit details
Add a shared floating music player backed by same-origin Astro API routes that proxy Navidrome/Subsonic requests without exposing
credentials to the browser.

- add server-only Navidrome helpers, media proxy routes, and typed environment keys
- add pure playback queue helpers with coverage for shuffle, repeat, and navigation behavior
- add the persisted player shell, client interaction module, responsive dialog styling, default art, and dock stacking support
- add music navigation triggers and regenerate IcoMoon assets for the new player controls

Merge branch 'chore/updates'

May 6, 2026asce1062View on GitHub0d1b039
Commit details
  • chore/updates:

refactor(ui): align interface utility usage

refactor(ui): align interface utility usage

May 6, 2026asce1062View on GitHub1f404f2
Commit details
Apply the shared button utility stack to the guestbook entry actions trigger
and remove duplicated local trigger chrome from the guestbook stylesheet.

- add ordered-list coverage to the style guide prose specimen
- update style guide button personality specimens to the primary variant
- adjust the home Hello action color and add a bug icon on the Hello page

Merge branch 'fix/miscellaneous-issues'

May 4, 2026asce1062View on GitHub5867d4c
Commit details
  • fix/miscellaneous-issues:

fix(layout): align offline assets and content footnotes

fix(layout): align offline assets and content footnotes

May 4, 2026asce1062View on GitHub7215c48
Commit details
Fix two miscellaneous production issues affecting content pages and
offline-capable asset handling.

- allow Workbox asset rules to match image/font/script URLs with query strings
- ignore Netlify dpl deploy query params during precache lookup
- prevent queried static assets from falling through to the offline page
- align MDX footnotes with the active sidebar width when the rail collapses
- animate footnote width and padding with the same sidebar timing

Merge branch 'fix/theme-url-flavor-param'

May 4, 2026asce1062View on GitHubdfded12
Commit details
  • fix/theme-url-flavor-param:

fix(theme): support URL theme and flavor overrides

fix(theme): support URL theme and flavor overrides

May 4, 2026asce1062View on GitHub9729ffc
Commit details
Add URL-driven appearance handling for theme and flavor while preserving
manual theme switching after a URL theme has been applied.

- apply ?theme=light|dark before paint without persisting it
- apply ?flavor=<id> before paint and persist explicit flavor selections
- retain stored/current flavor when a later URL only provides ?theme=
- keep shareable URLs intact when users toggle theme manually
- preserve match-device behavior when no URL theme override exists
- add regression tests for URL theme toggles and flavor retention

Verification:
- npx vitest run src/scripts/tests/themeManager.test.ts

Merge branch 'feature/style-guide-page'

May 4, 2026asce1062View on GitHubd73f488
Commit details
  • feature/style-guide-page:

feat(style): add personal style guide page

feat(style): add personal style guide page

May 4, 2026asce1062View on GitHubf671bb2
Commit details
Add /style as a writing-first field manual for alexmbugua.me, with
editorial voice rules, title conventions, formatting guidance, technical
writing standards, rule-breaking notes, and before/after examples.

- document supporting interface specimens for tokens, type, prose, links,
badges, buttons, forms, controls, cards, and terminal accents
- add the Style route to Explore navigation
- update the IcoMoon font build with the style-page icon set
- update the IcoMoon preload cache key in the shared layout

Merge branch 'chore/palette-theme-flavors'

May 4, 2026asce1062View on GitHub31a6f16
Commit details
  • chore/palette-theme-flavors:

feat(palette): document theme flavor tokens

feat(palette): document theme flavor tokens

May 4, 2026asce1062View on GitHubfb26d83
Commit details
Expand the design token registry so the palette page reflects the current
light, dark, and flavor-aware color system.

- add semantic color roles, base theme OKLCH values, and all theme flavor token sets
- add a palette Flavors tab with dark/light token grids and copyable OKLCH values
- update email and console consumers to use current semantic token mappings
- resolve console colors from live CSS variables so active flavors affect the console egg
- add the flask-florence icon to IcoMoon and update the font preload cache key

Merge branch 'update/blog-content'

May 4, 2026asce1062View on GitHubd20e945
Commit details
  • update/blog-content:

update(content[blog]): add one about being brave enough to begin

update(content[blog]): add one about being brave enough to begin

May 4, 2026asce1062View on GitHube3892e6

Merge branch 'fix/avatar-exploratory-state'

May 1, 2026asce1062View on GitHubad7f9f0
Commit details
  • fix/avatar-exploratory-state:

fix(avatar): discard unsaved exploratory state

fix(avatar): discard unsaved exploratory state

May 1, 2026asce1062View on GitHub19f775f
Commit details
Keep avatar edits exploratory until the user explicitly saves or opts in
through guestbook avatar submission.

- initialize the full avatar generator from saved localStorage state or defaults
- prevent randomize, layer edits, and gender changes from rewriting the page URL
- build share URLs on demand from the current exploratory avatar
- reset mini avatar widgets to saved/default state when they initialize
- preserve guestbook opt-in as an explicit save path
- add focused coverage for saved/default initialization and exploratory edits

Merge branch 'fix/avatar-generator-responsive-layout'

May 1, 2026asce1062View on GitHub82a975d
Commit details
  • fix/avatar-generator-responsive-layout:

fix(avatar): make generator layout fully responsive

fix(avatar): make generator layout fully responsive

May 1, 2026asce1062View on GitHub274b7a3
Commit details
Center the 8-bit avatar generator once the preview and selection areas
collapse into a single column, so the picker no longer sits left-aligned
during intermediate viewport widths.

- constrain preview and selection sections to the same stacked width
- center the selection section and layer picker in single-column layout
- keep desktop side-by-side layout and mobile picker height behavior intact
- preserve the updated avatar save/share action treatments

Merge branch 'fix/avatar-generator-responsive-selection'

May 1, 2026asce1062View on GitHub04c62c8
Commit details
  • fix/avatar-generator-responsive-selection:

fix(avatar): improve generator controls responsiveness

fix(avatar): improve generator controls responsiveness

May 1, 2026asce1062View on GitHub23ba6ec
Commit details
Refine the 8-bit avatar generator layout so the layer picker and preview
controls stay balanced across desktop and mobile views.

- make the layer tabs compact and shrink-wrapped when all tabs fit
- keep horizontal tab scrolling available only for narrow views
- preserve mobile picker height while increasing desktop selection height
- move save and share into icon actions below the avatar preview
- use primary-to-accent icon states for save/share hover and focus
- keep Randomize and Download as the main lower action buttons

Merge branch 'fix/mobile-terminal-neofetch-size'

May 1, 2026asce1062View on GitHub7a22d83
Commit details
  • fix/mobile-terminal-neofetch-size:

fix(terminal): tune mobile neofetch text scale

fix(terminal): tune mobile neofetch text scale

May 1, 2026asce1062View on GitHub68dd798
Commit details
Reduce the floating terminal's mobile neofetch/system output text so it no
longer appears larger than the init messages.

- size mobile system art and metadata rows to match the smaller status text
- reduce the system font label one step further for visual hierarchy
- keep regular terminal log, command, and init message sizing unchanged

feat(notification): redesign share feedback toast

May 1, 2026asce1062View on GitHub36780c6
Commit details
Replace the flat share notification with a launch-stamp treatment that better
matches the site's retro-cosmic visual language.

- add a rocket sigil rail and compact stamped notification layout
- move notification-specific CSS out of global.css
- keep the existing framed error close button treatment
- show the dynamic copy message as muted supporting text
- rotate through common and rare static notification titles on display
- replace the plain slide animation with a clipped launch/depart motion

Verification:
- npm run test -- src/scripts/tests/feedbackManager.test.ts

fix(notification): use shared button styling for close control

April 30, 2026asce1062View on GitHub9d5b6e1
Commit details
Replace the custom share notification close-button styles with the existing
button classes so the control matches the site's framed error button treatment.

- apply shared btn, btn-frame, btn-error, and btn-xs classes to the close button
- switch the close icon to the larger x-lg glyph
- remove the old notification-only close-btn CSS

fix(navbrand): stabilize sidebar text-effect state

April 30, 2026asce1062View on GitHubd87b34b
Commit details
Keep nav-brand copy ownership inside the coordinator so hover, resume,
return, and terminal-context messages do not race through DOM mutations.

- drive greeting, teaser, and visit-count updates through the shared text
effect API
- respect each element's declared random-effect configuration when the
coordinator starts an effect
- keep hover/focus terminal affordances scoped to the teaser line
- delay return-to-active settling until the return greeting finishes
- gate terminal-specific sidebar greetings until the terminal is open or docked
- remove obsolete cursor and hint-state exports after the prompt cursor moved
into the typing effect

Verification:
- npm run test -- src/lib/tests/navBrandMessages.test.ts

feat(sidebar): add nav-brand email copy and visit count

April 30, 2026asce1062View on GitHubc1eef7e
Commit details
Add lightweight utility interactions to the expanded sidebar nav-brand.

- copy the site email from the nav-brand identity line using the shared
clipboard/notification flow
- render the visit count after the shell prompt path with the shared typing
effect
- remove the redundant trailing underscore cursor and its cursor-mode styling

feat(share): prefer native device share

April 30, 2026asce1062View on GitHub54a726a
Commit details
Add shared native-share-first behavior for site share surfaces while keeping
clipboard copy as the fallback.

- add shareOrCopyToClipboard to centralize Web Share API handling
- use native share for blog post, avatar, and guestbook entry share actions
- keep copy-only guestbook actions on the existing clipboard path
- center the blog share label and icon row without breaking mobile wrapping
- treat native share cancellation as a no-op instead of copying unexpectedly
- cover native share success, cancellation, unavailable API, and fallback copy

Verification:
- npx vitest run src/scripts/tests/feedbackManager.test.ts

fix(toc): offset anchors below sticky contents

April 30, 2026asce1062View on GitHubd2bbe6d
Commit details
Keep headings visible when selecting entries from the sticky table of
contents.

- define the default anchor scroll offset as a layout token
- measure the rendered TOC height at runtime and publish it to the token
- use the token for heading scroll margins instead of a fixed value
- update the offset after details toggle and viewport resize events

feat(social-share): add fediverse and forum targets

April 30, 2026asce1062View on GitHub6d0360c
Commit details
Add Bluesky, Reddit, Mastodon, and LinkedIn targets to the content share
controls.

- use updated IcoMoon classes for the new platform icons
- refresh generated IcoMoon font assets and demo metadata
- update the Layout font preload hash to match the regenerated woff asset

fix: improve low-brightness atmosphere tokens

April 29, 2026asce1062View on GitHub3044a05
Commit details
Separate decorative atmosphere colors from structural surface tokens so
background texture, matrix rain, and guestbook card patterns remain legible
without brightening borders, cards, or base surfaces.

- adds per-theme and per-flavor ink roles for:
- atmosphere texture
- guestbook card patterns
- matrix rain glyphs
- updates topography masks in global and sidebar styles to use atmosphere ink
- updates matrix rendering to read the matrix rain token
- gives sidebar flavor swatches fixed representative colors
- moves guestbook browser card, preview, and swatch masks to guestbook pattern ink
- updates guestbook notification and entry-copy emails to recolor notecard SVGs with email-safe pattern ink
- isolates missing-pepper hash tests from local GUESTBOOK_HASH_PEPPER values

chore(misc): update terminal scanlines and footnotes styling

April 22, 2026asce1062View on GitHub81c8a67
Commit details
Updated terminal-window__body scanlines to be more visible and theme-aware

- Moved scanlines into terminal-window__body::before so they overlay the terminal body without affecting layout.
- Increased line visibility from the old very subtle 2% content mix to 9%.
- Used a 2px transparent / 2px line rhythm similar to your reference.
- Added a subtle secondary-color sweep so it feels integrated with the terminal theme.
- Raised log and prompt content above the overlay with z-index: 1.

Updated footnotes styling for better readability and aesthetics
- Add numbering styles to the <ol> inside .footnotes:

feat(text-effects): add lifecycle motion language

April 22, 2026asce1062View on GitHub0bb2362
Commit details
Redesign the terminal text effect engine from reveal-only playback into lifecycle-aware motion with effect metadata, paired enter/exit
families, standalone flourishes, cancellation, reduced-motion fallbacks, and declarative trigger coordination.

Add type, cipher, and rare signal families: typing/backspace, decrypt/entropy, and glitch-lock-on/signal-loss. Tune short/long text
pacing, terminal block cursor behavior, backspace realism, signal dropout holds, and rare signal visuals so they read differently from cipher
effects.

Add content-change, delayed route-enter settling, bind-time load delay support, hover replay guards, random-effect eligibility, and
registry-backed sidebar/header/terminal flourish wiring through data-text-effect attributes.

Move sidebar navbrand flourishes out of navBrand state policy and into declarative Sidebar.astro markup. Remove the navbrand-specific
effects adapter and transition-effect chooser so navBrand remains responsible for state, copy, cursor, and terminal nudges only.

Update tests for metadata, parser support, reduced motion, paired transitions, interruption restoration, rare signal behavior, content-
change, route-enter settling, sidebar declarative behavior, and navbrand state cleanup.

feat(text-effects): wire paired motion transitions

April 22, 2026asce1062View on GitHub1915839
Commit details
Promote changed stable text from enter-only reveal into full paired transitions so typing uses backspace first, decrypt uses entropy first, and rare lock-on effects can use signal-loss.

Wire navbrand through runTerminalTextTransition instead of reset-then-reveal so the previous greeting remains available for the exit phase.

Add rare-family motion metadata and renderers for glitch-lock-on and signal-loss while keeping rare effects opt-in through explicit data-text-effect declarations.

Expand header greeting and terminal atmosphere declarations with reversible type/cipher exit effects and preserve terminal atmosphere stable text with data-text-effect-stable-text.

Document the motion model: public effect names, internal families, lifecycle roles, standalone safety, stable text caching, and paired-transition inference.

Expand focused tests for rare metadata/parser support, paired full-transition promotion, rare transition playback, and navbrand paired-transition behavior.

feat(text-effects): build terminal motion foundation

April 22, 2026asce1062View on GitHub4b37a31
Commit details
Introduce effect metadata for terminal text effects, including family, lifecycle role, standalone eligibility, and reduced-motion strategy.

Add runTerminalTextTransition as the lifecycle-aware coordinator for enter-only, exit-only, standalone, and full text transitions.

Split playback into renderer functions for typing, backspace, decrypt, and entropy instead of continuing to expand the old branch-heavy playback path.

Centralize reduced-motion fallback behavior in the generic engine and add a neutral data-text-effect-stable-text cache while preserving data-greeting-target compatibility.

Decouple navbrand effect typing from hardcoded typing/decrypt values so the shared engine owns the generic effect vocabulary.

Expand terminal text effect tests for metadata, parser support, reduced motion, standalone backspace, backspace-to-typing transitions, cancellation restoration, entropy, and standalone-safe randomization.

feat(navbrand): polish terminal command surface

April 22, 2026asce1062View on GitHub25d7cd8

feat(navbrand): polish terminal command output

April 22, 2026asce1062View on GitHub4f8db89

feat(navbrand): strengthen terminal command base

April 21, 2026asce1062View on GitHub2a97d35

feat(navbrand): refine terminal command output

April 21, 2026asce1062View on GitHub4000c60

feat(navbrand): add staged terminal system profile

April 18, 2026asce1062View on GitHubd2ad500

feat(navbrand): update sidebar teaser assets and styles

April 18, 2026asce1062View on GitHub61551b7

feat(navbrand): polish terminal interactions and button styles

April 18, 2026asce1062View on GitHub93b28e3

feat(navbrand): add terminal teaser entry points

April 17, 2026asce1062View on GitHub122277a

feat(navbrand): move commands into terminal modal

April 17, 2026asce1062View on GitHub25dc177

feat(navbrand): add shared terminal modal shell

April 17, 2026asce1062View on GitHub02b7166

refactor(navbrand): share terminal open event contract

April 17, 2026asce1062View on GitHub19f57b5

refactor(navbrand): restore sidebar terminal teaser

April 17, 2026asce1062View on GitHub5461592

test(navbrand): harden terminal helper contracts

April 17, 2026asce1062View on GitHubc115b51

test(navbrand): add terminal window and prelude helpers

April 17, 2026asce1062View on GitHubebb598c

feat(navbrand): add action command intents

April 17, 2026asce1062View on GitHuba96a6ae

feat(navbrand): add search handoff support

April 17, 2026asce1062View on GitHub0251036

feat(navbrand): add typed command launcher

April 17, 2026asce1062View on GitHub526b6d9

feat(navbrand): align phase 3 launcher foundation

April 17, 2026asce1062View on GitHub84a65ab

docs(effects): clarify flourish contracts and extension points

April 17, 2026asce1062View on GitHub2faa281

refactor(effects): add declarative text effect registry

April 17, 2026asce1062View on GitHub955cf70

feat(effects): expand shared text effect triggers

April 17, 2026asce1062View on GitHub09900b0

refactor(effects): share terminal text transitions

April 17, 2026asce1062View on GitHub5547554

feat(navbrand): add phase 2 transition effects

April 17, 2026asce1062View on GitHub9ed0c10

feat(navbrand): add phase 1 presence engine coordinator

April 17, 2026asce1062View on GitHub961937b

test(navbrand): add phase 1 message and state helpers

April 17, 2026asce1062View on GitHubfda0a63

fix(icomoon): font preload warning

April 16, 2026asce1062View on GitHub9cb9368
Commit details
Reordered the IcoMoon @font-face sources to prefer woff before ttf.

Diagnosis:
- Layout.astro preloads /fonts/icomoon/fonts/icomoon.woff?4weq0r.
- But icomoon/style.css declares ttf before woff in @font-face.
- So the browser could preload woff and then still choose ttf,
which triggers the "preloaded but not used" warning.

refactor(sidebar): refine sidebar behavior and unify ascii widget handling

April 16, 2026asce1062View on GitHubb396680

refactor(security): rename guestbook hash salt to pepper and harden prod config

April 16, 2026asce1062View on GitHub865ab36

Merge branch 'feat/navbrand-redesign'

April 16, 2026asce1062View on GitHub5fb4aed
Commit details
  • feat/navbrand-redesign:

fix(navbrand): guard NaN corruption, negative elapsed, and sessionStorage exceptions
chore(privacy): document nav-brand-visits and nav-brand-last-visit keys
fix(navbrand): collapse hidden sub-row and increase max-height for expanded content
feat(navbrand): replace nav-brand-prompt CSS with shell prompt styles
feat(navbrand): update Sidebar.astro HTML structure
feat(navbrand): add DOM rendering and event listeners
feat(navbrand): add pure functions with unit tests
chore(prefs): add navBrandVisits and navBrandLastVisit keys

fix(navbrand): guard NaN corruption, negative elapsed, and sessionStorage exceptions

April 16, 2026asce1062View on GitHubaf10b86

chore(privacy): document nav-brand-visits and nav-brand-last-visit keys

April 16, 2026asce1062View on GitHube1b6b12

fix(navbrand): collapse hidden sub-row and increase max-height for expanded content

April 16, 2026asce1062View on GitHub7374f78

feat(navbrand): replace nav-brand-prompt CSS with shell prompt styles

April 16, 2026asce1062View on GitHubaeb4d60

feat(navbrand): update Sidebar.astro HTML structure

April 16, 2026asce1062View on GitHub120cffe
Commit details
Replace badge-stack nav-brand with multi-line visitor-aware shell prompt
layout and wire up the navBrand script import.

feat(navbrand): add DOM rendering and event listeners

April 16, 2026asce1062View on GitHub0456016

feat(navbrand): add pure functions with unit tests

April 16, 2026asce1062View on GitHub9b38293

chore(prefs): add navBrandVisits and navBrandLastVisit keys

April 16, 2026asce1062View on GitHub88b0c5f

feat(tab): tiered idle title rotation with 12-message set

April 15, 2026asce1062View on GitHub28479cf
Commit details
Three tiers keyed to idle duration
— playful (0-10s), system (10-30s), cosmic (30s+)
— cycling every 8s via setInterval. Index resets on tier
transition. Timer clears on tab focus restore.

chore(privacy): document three new localStorage keys

April 15, 2026asce1062View on GitHub16fffcc
Commit details
Added theme-flavor, theme-transition, and sidebar-collapsed to all
privacy disclosures. All three disclosure files now
consistently declare nine localStorage keys.

- src/pages/privacy.astro
- six to nine
- added three <li> entries
- src/components/PrivacyPolicy.astro
- five to nine
- updated prose
- public/.well-known/dnt-policy.txt
- six to nine
- three new bullets
- "contact us" to "contact me"
- reassertion date 2026-04-15

fix(pwa): resolve service worker 404 on Netlify SSR builds

April 14, 2026asce1062View on GitHubf5c531c
Commit details
Update @vite-pwa/astro to pkg.pr.new/astro@73 (Astro 6 / Vite 6
compatibility pre-release) and add a custom pwa-sw-copy-to-dist
integration to bridge the build output mismatch.

Root cause: @astrojs/netlify v7 sets config.build.client to
.netlify/build/ (the SSR function directory). @vite-pwa/astro reads
this at config-setup time and writes sw.js + workbox-*.js there
instead of dist/ where Netlify serves static files.

The custom integration hooks into astro:build:done (runs after the
PWA integration) and copies sw.js and workbox-*.js from .netlify/build/
to dist/, falling back silently on local static builds where
.netlify/build/ is absent.

fix(pwa): set explicit globDirectory to suppress Workbox precache warnings

@astrojs/netlify v7 sets config.build.client to .netlify/build/ (the
SSR function bundle directory). @vite-pwa/astro picks this up as
globDirectory, finds no CSS/HTML/font assets there, and emits
"glob pattern doesn't match any files" warnings (leaving the precache
manifest empty).

Explicitly setting globDirectory to dist/ in the workbox config takes
precedence over the integration's auto-detected value, pointing Workbox
at the correct static output directory where all built assets live.

feat(sidebar): add collapsible desktop sidebar rail with bookmark tab

April 14, 2026asce1062View on GitHub52ec810
Commit details
Implements a two-state collapsible sidebar for desktop (lg+).

Key design decisions:
- Sidebar width animation is the primary motion driver.
- All other elements follow its lead via CSS transitions and timing.
- Overflow switches to visible after the width animation completes
(transition: overflow 0s delay trick) so tooltip ::after pseudo-elements
can escape the panel once settled
- Nav-text uses opacity-only fade with a delayed max-width snap (no squish)
- Section labels, hidden sections, and footer are hidden via visibility:hidden
with no transition (the sidebar clip handles their visual disappearance)
- Header brand (prompt <> avatar) crossfades via opacity only
- Active item box-shadow removed in collapsed rail (too wide for 3.5rem)
- State persisted to localStorage and re-stamped on astro:after-swap to
survive Astro ClientRouter soft navigations
- Easter egg hidden instantly (display:none) to prevent flicker animation
playing during collapse
- Bookmark tab sits at z-39 (behind sidebar z-40) and slides out 14px on
hover via transform — no clip-path needed
- CSS-only tooltips (::after/::before) on nav-link[data-tooltip] in
collapsed mode, keyboard accessible
- Preference key: PREF_KEYS.sidebarCollapsed in lib/prefs.ts
- --sidebar-current-width CSS var drives both sidebar width and content
margin-left offset
- Desktop only, behavior unchanged on mobile

Files: Sidebar.astro, Layout.astro, sidebarCollapse.ts, sidebar.css,
prefs.ts, global.css, theme.css, icomoon (collapse/expand icons)

Squashed commit of the following:

April 13, 2026asce1062View on GitHub3b6f5ee
Commit details
commit 361941368c9fa99c553cefde47c5284550c0ef6c
Author: asce1062 <tnkratos@gmail.com>
Date: Thu Apr 9 14:46:55 2026 +0300

chore(ctv2): misc adjustments

commit 320a1092796cb7283215f2c4bfad14ed6fa7a986
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 19:51:44 2026 +0300

feat(transitions): add switchFlavor - resolves effective style and calls atomicSwitch

commit 6ad6f423efa7ffc435fae9e565adb9d10de20b09
Author: asce1062 <tnkratos@gmail.com>
Date: Thu Apr 9 00:22:10 2026 +0300

feat(transitions): add transition pill styles to sidebar CSS

commit b4b20f466fe726378d2a9895b0c33bc9cf90ead2
Author: asce1062 <tnkratos@gmail.com>
Date: Thu Apr 9 00:05:12 2026 +0300

feat(transitions): implement scanline handler with CRT raster bands and halo beam

commit 31cf6cf9c0d6fd806d62f3acbe57e405167e0993
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 19:33:42 2026 +0300

feat(transitions): add transition manager - pref read/write and pill sync

commit c968445f7082624e68a60d032d1eba6575e70b79
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 19:58:55 2026 +0300

feat(transitions): pass context to atomicSwitch in handleThemeToggle

commit 3ad7b1a646113161eb0fbded9deeeb4e17b05235
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 19:45:54 2026 +0300

refactor(transitions): atomicSwitch dispatches to registry with cancel-and-replace guard

- Replace themeTransition.ts: atomicSwitch now accepts TransitionContext,
resolves style from stored pref, guards against terminal on init/soft-nav,
cancel-and-replaces any in-flight transition, dispatches to registry handler
- Add transitionState.ts: isolated cleanup-closure state to break circular dep
- Re-export registerTransitionCleanup from themeTransition for backward compat
- Deprecate initTransitionOverlay as a no-op stub (handlers own the overlay)
- Add "programmatic" to TransitionContext.source union in transitionRegistry

commit 5465dbf6f863817ebe280b6cc5e9c2d2cc3f179c
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 14:04:59 2026 +0300

feat(transitions): add transition registry - types, mapping, pure helpers, none handler

Introduces transitionRegistry.ts with:
- TransitionStyle union type and TRANSITION_STYLES constant
- FLAVOR_TRANSITION_MAP covering all 8 flavors
- getMappedTransitionForFlavor() and resolveEffectiveTransitionStyle() pure helpers
- TransitionContext interface and TransitionFn type
- Exported getOverlay/wait/clamp helpers for use by overlay-based transition handlers (scanline, phosphor, retune, etc.)
- Full transitionRegistry record with none handler + stubs for all 8 styles
- 13 tests covering map coverage, per-flavor lookups, and resolve priority

commit 8ace8afc5b3d523fc39825bb708e2c5d8381cbcd
Author: asce1062 <tnkratos@gmail.com>
Date: Wed Apr 8 13:57:03 2026 +0300

feat(transitions): add PREF_KEYS.transition preference key

commit b0fe9f572076beb80bafdf88bcd6b42ac80da525
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 23:15:43 2026 +0300

feat: 7-flavor optional theme system and dark/light overhaul

Add a flavor picker to the sidebar with 8 opt-in color overlays (default +
crt-green, amber, synthwave, dos, void, ice, redline). Each flavor overrides
OKLCH color tokens on html[data-theme="dark"][data-flavor="X"] for dark-mode-
native theming without touching light mode.

- theme.css: 7 flavor blocks (full OKLCH palette per flavor), boosted dark
base surface chroma (base-200/300 from 0.004→0.010/0.012 at hue 270),
semantic tokens (--color-heading, --color-link, --color-muted, --color-code-bg)
- global.css: split coupled h2+a rule into separate heading/link semantic tokens
- flavorManager.ts: flavor type, FLAVORS/LABELS/SWATCHES exports, setFlavor,
applyStoredFlavor, syncSwatches, initFlavorPicker with AbortController
- flavorPicker.ts: entry script wired to astro:after-swap + astro:page-load
- prefs.ts: add flavor key ("theme-flavor")
- Layout.astro: early-init is:inline script stamps data-flavor before paint
- Sidebar.astro: flavor section with 8 swatch buttons (data-flavor-btn)
- sidebar.css: .nav-flavor-picker/.nav-flavor-swatch styles with active glow

commit 5b9491ba74c5ce0a123d092593133ab5455b7945
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 22:50:10 2026 +0300

feat: semantic color tokens and warm void depth

- Add --color-heading/--color-link/--color-muted/--color-code-bg semantic tokens
to decouple h2 and link colors (previously both --color-warning = no hierarchy)
- Split h2, a { color: warning } into separate rules using semantic tokens:
h2 → --color-heading (amber, phosphor warmth)
a → --color-link (orange in light, coral in dark)
- Boost dark base surface chroma: base-200/300 from 0.004 to 0.010–0.012
at hue 270 to match base-100's purple ghost - surfaces now feel chromatic
- Strengthen dark mode glow tokens: active +4px, accent +4px, identity +8px
- Deploy --glow-accent to three surfaces: link-action hover, input focus,
layer tab active state

commit e3832a7de8215ce09b4e1ba5c5e54b04cf887ade
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 22:00:18 2026 +0300

fix: a11y, hardening, and greeting polish

- fix(404): add sr-only h1 to restore heading hierarchy (WCAG)
- fix(preview): replace shadow-xl with elevation token
- fix(avatar): layer tab icons visible (color: transparent → token)
- fix(avatar): add role=tablist/tab + aria-selected to LayerTabs
- fix(avatar): sync aria-selected on switchLayer + gender/ext changes
- fix(guestbook): AbortController cleanup for document-level listeners
- fix(guestbook): ArrowUp/Down keyboard nav for role=menu entry actions
- fix(guestbook): auto-focus first menuitem on open; borderRadius → 0rem
- fix(guestbook): expand entry-actions-trigger tap area to 44px (WCAG 2.5.5)
- fix(sidebar): expand nav-avatar-btn to 44px tap target
- fix(sidebar): most-specific active link match (no false /blog highlight)
- fix(search): center floating modal within content area on lg+ (sidebar offset)
- fix(greeting): inline-block constrains hover zone to text width only
- fix(greeting): period now scrambles in decrypt animation

commit 083be1a0b36aaa401d2d02eeb8d12bc96537af45
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 21:22:14 2026 +0300

chore: polish scope transitions and fix listener leaks

Scope transition-all → specific GPU-safe properties across components:
- Header, ThemeSwitcher, guestbook random btn: transition-[color,transform]
- ProjectCard link: transition-colors (color changes only)
- PreviewImage: transition-transform (scale only)
- BackToTop: transition-[opacity,transform,visibility] (fade + scale)
- scrollProgress: transition-[width] (only width changes)

Zero-radius: BackToTop rounded-full → rounded-none (UI control, not avatar)

scrollProgress: add AbortController so scroll listeners don't accumulate
across Astro soft navigations (same pattern as sidebarNav, FloatingSearch)

commit f8d1ab857d017927575e7a1a2743acc759645b77
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 21:14:16 2026 +0300

fix: harden FloatingSearch against listener accumulation

Memory leak: window/document event listeners (resize, scroll, orientationchange,
keydown x2) and ResizeObserver were never cleaned up between Astro soft
navigations, causing them to pile up on each astro:page-load reinit.

Fix: Add AbortController (_ac) and ResizeObserver ref (_ro) at module scope.
Each init tears down the previous set before registering new listeners -
identical pattern to sidebarNav.ts.

Also:
- Restore focus to the triggering element on modal close (accessibility)
- Replace setTimeout(fn, 100) focus call with requestAnimationFrame
- Scope transition-all → transition-opacity on modal div (GPU path only)

commit 88b7ce7be00be724f47f77b6e47081744e162cc3
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 21:08:59 2026 +0300

perf: optimize font loading and containment

Font loading:
- Add <link rel="preload"> for 0xProto-Regular, 0xProto-Bold, 0xProto-Italic,
and icomoon.woff before CSS is parsed - reduces FOUT window for body text
and nav icons

CSS containment:
- Add contain: layout style to .nav-menu (sidebar is a self-contained panel;
internal changes no longer trigger external layout recalculations)

Transition scope:
- AvatarGenerator layer items: transition:all 0.2s → specific border-color/box-shadow
- ContentCard link: transition-all duration-300 → transition-colors duration-200

commit abec107b6cfd0f374a2ff04bbea96ac87427f528
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 20:52:11 2026 +0300

chore: centralize and tokenize all transition/animation literals

Replace every hardcoded timing literal (0.1s–0.3s) and bare ease keyword
across the CSS system with motion design tokens. Scope broad transition: all
to specific properties for GPU-accelerated paths only.

Files updated:
- global.css, sidebar.css, collapsible-nav.css - dark-mode, nav, and chevron transitions
- btn.css, toggle.css - interaction feedback timings
- link-action.css - already committed in previous session
- input.css, checkbox.css - utility component transitions
- guestbook.css - entry bubble, swatch, actions trigger, action item
- search.css - pagefind clear button, result row, result link
- avatar.css - gender-option, layer-tabs, layer-tab (narrowed from transition:all)

Intentional exceptions kept as literals:
- asciiWidget.css: diceSpin 0.5s (one-shot effect)
- guestbook.css: entry-highlight 1.5s (narrative anchor animation)
- asciiReveal.css: art-scan, flicker sequences (effect timings)

commit 66b054482e3a1e8a4ba6d59813fb73a01bfc1c22
Author: asce1062 <tnkratos@gmail.com>
Date: Mon Apr 6 20:29:11 2026 +0300

chore: align with standards

- guestbook.css entry-bubble + textarea-wrapper border-radius
0.25rem → 0rem; defends zero-radius identity in guestbook UI
- global.css share-notification box-shadow → var(--elevation-3);
sidebar.css 800ms literal → var(--duration-crawl) var(--ease-out);
link-action.css 0.2s literals → var(--duration-base)
- meta.astro + guestbook.astro rounded-md/rounded-none;
all public pages now comply with zero-radius identity
- heading transition-all → transition: font-size var(--duration-instant)
on h1/h2/h3/.site-greeting; scoped to font-size only, not all properties

commit 4480359046d5e9bf1cfcab604255f4e633958ebe
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 22:32:40 2026 +0300

chore: update layout and language

- remove b{color:warning} from components layer.
- bold text in prose no longer inherits warning color
- remove h4 from warning color selector (h2,h4,a → h2,a);
- h4 is rarely used and warning color should be intentional, not default
- humanize sidebar option descriptions
- Avoid mechanical phrasing and jargon; now conversational
- aria labels updated from generic "Visit the X page"
- pattern to action-oriented descriptions (read, browse, sign, search)
- about.astro resume copy - "digital version of my resume
available here" → "My resume is available here"

commit f45d1d7688d0686b1738f3f9a7b47115f5cf758e
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 17:01:26 2026 +0300

feat: visual identity push

- faded-text gradient reduced from 5-stop rainbow to 2-stop
primary→accent (warm void palette only); references motion tokens
- add glow token vocabulary to theme.css - glow-active,
glow-accent, glow-identity
- apply glow-active to active nav link (inset coral wash);
apply glow-identity text-shadow to site greeting
- homepage exit section - Hello link-action upgraded to
link-action--primary; one clear primary CTA vs three equal-weight items
- greetingReveal.ts - site greeting decrypts through CRT block
drawing characters on page load, mouseenter, and
touchstart; cancel-and-restart on rapid re-trigger; reduced motion
respected; content always visible before JS runs
- sidebar brand prompt - blinking cursor added after :~$ badge
- greeting scramble chars using terminal block/box-drawing glyphs
- nav section labels - remove success badge from section names;
replace with .nav-section-name plain text at neutral-content color;
6 fewer competing badges across main/explore/meta/options sections
- contact prominence on homepage - Hello and Verify link-actions
in exit section; bio copy revised

commit c04b69965f45a972ed0dcd8f236fd5a4a6eeaecd
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 16:45:54 2026 +0300

chore: layout composition and spacing rhythm

- homepage section spacing mb-3/mb-6 → mb-8/mb-12; terminal
commands now breathe like a real shell session
- main page wrapper pb-4/pb-8 → pb-8/pb-12; grounded bottom
- about.astro exit section mb-6 → mb-10; consistent section rhythm
- header #navigation mb-3 → mb-4; tighter breadcrumb clearance
- ContentLayout metadata - display:block with mt/mb, replace
<br><br> hack with proper spacer; date/tags visually grouped
- heading margins in markdown.css - article h2/h3/h4 get
margin-top/bottom for reading rhythm; first-child exceptions suppress
double-spacing at section tops
- skip-link border-radius 0 0 4px 0 → 0; defends zero-radius
identity
- share-notification box-shadow rgba → oklch for color consistency

commit 5cbbda715fb3145a4f854b1eac8720c7d867d93c
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 16:36:42 2026 +0300

chore: foundation, correctness and token alignment

- remove rounded-md from ThemeSwitcher (breaks zero-radius identity)
- remove rounded-md from Header search button (same pattern)
- aria-hidden decorative blink cursors across 13 pages
- remove color override from b{} in global.css
- shadow tokens → oklch; add motion token vocabulary to theme.css
- sidebar overlay → oklch color space
- code highlight color → color-mix warm void tokens
- char-id syntax highlight → primary/accent/secondary tokens
- collapsible-nav references motion tokens; grid refactor deferred to phase 5
- fluid heading scale with clamp() scoped to article (content pages only)

fix(index): tighten mobile spacing to fit one viewport without scroll

April 4, 2026asce1062View on GitHubb8ad5a2
Commit details
Reduce paragraph margins from 1em to 0.4em on mobile (<768px) via scoped
style. Shrink inter-section gap from mb-6 to mb-3 on mobile (md:mb-6 on
desktop). Saves ~70px vertical space on 375px-wide devices; desktop layout
is unaffected.

Squashed commit of the following:

April 4, 2026asce1062View on GitHube92baea
Commit details
commit 3b792f0a8a40cfeaecbd139a3da1b0e3057710cd
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 04:40:54 2026 +0300

feat: add Matrix rain canvas background with theme-awareness and mutual exclusion

- New sidebar toggle "Matrix" (icon-columns) wired to matrixBackground.ts
- Canvas injects into body, renders hiragana rain at ~30fps via rAF
- Colors read from CSS custom properties at runtime: --color-base-100 (trail)
and --color-base-300 (characters) so the effect adapts to light/dark theme
- MutationObserver on data-theme clears canvas and refreshes colors instantly
when the user switches themes — no stale trail bleeding through
- Mutual exclusion via custom DOM event "background:activate": enabling Matrix
fully disables Stars (stops rAF loop, removes canvas, unchecks toggle) and
vice versa; cross-tab sync handled symmetrically via storage event
- Reduced-motion guard: canvas is built but animation does not start when
prefers-reduced-motion is set
- Early-apply inline script in Layout.astro stamps data-matrix-bg before paint
to prevent FOUC on page load
- Added matrixBackground pref key to PREF_KEYS, sidebar option to navigation.ts
- Updated privacy.astro and dnt-policy.txt: five → six localStorage values,
added matrix-bg entry, updated REASSERTION date to 2026-04-04

Squashed commit of the following:

April 4, 2026asce1062View on GitHubd17090b
Commit details
commit 496026e37c5aef23e7cd56a27f0a49fa62eab269
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 03:01:20 2026 +0300

feat: inactive tab title with rotating messages

commit ce14fc8806858673856f98dc3ac2104c69f4d580
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 02:48:26 2026 +0300

feat: inactive tab title with rotating messages

Swaps document.title to a custom message when the user
switches away from the tab, restoring the real title on return.

- visibilitychange listener registered once at module scope (no duplicates across soft navs)
- astro:page-load re-captures activeTitle so soft navigations restore the right page title
- OG/SEO unaffected: og:title and <title> are server-rendered; this is client-only

Squashed commit of the following:

April 3, 2026asce1062View on GitHub9b427be
Commit details
commit 43770944a7e3178f5a5790103eb0d65d14526615
Merge: 447ed35 fc16c6a
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 01:06:32 2026 +0300

merge: fix/visual-fixes → feat/warm-void-redesign

* fix/visual-fixes:
fix: visual fixes. Icon sizing, changelog layout, avatar save policy, token cleanup

commit fc16c6a8ce5682c0ef4f35969c0d394494c7d984
Author: asce1062 <tnkratos@gmail.com>
Date: Sat Apr 4 01:05:58 2026 +0300

fix: visual fixes. Icon sizing, changelog layout, avatar save policy, token cleanup

- Unify theme/search icon buttons to h-11 w-11 with flex centering
- Changelog: commit title on its own line; short SHA badge pushed right
- Avatar: persist to localStorage only on explicit Save
— remove all auto-persist from randomize, layer change, and gender switch

commit fa73bd408bc000c2802e50abf1d70576f6fc53af
Merge: f064325 f0d01fc
Author: asce1062 <tnkratos@gmail.com>
Date: Tue Mar 31 23:23:42 2026 +0300

merge: chore/phase4-polish → feat/warm-void-redesign (Phase 4 polish)

* chore/phase4-polish:
chore: UI/UX polish. Elevation tokens and UX fixes

commit f0d01fcf8c674a68f9ac6bec5662453312fa340c
Author: asce1062 <tnkratos@gmail.com>
Date: Tue Mar 31 23:23:08 2026 +0300

chore: UI/UX polish. Elevation tokens and UX fixes

Elevation tokens:
- theme.css: add --elevation-0/1/2/3/focus glow-based system

Polish:
- scrollProgress.ts: replace status-color gradient with brand tokens; add glow box-shadow
- guestbook.css: .entry-bubble hover border-color transitions to accent
- .nav-link--active gains 2px accent left border for clear active page indicator
— Mobile body font-size 14px → 15px
— Nav link min-height 2rem (32px) → 2.75rem (44px)

Minor fixes:
- sidebar.css: nav-section-label inline-flex → flex for full-width sticky coverage
— Sidebar <aside>: add role="navigation" (overrides implicit
complementary role) and update aria-label to "Main navigation"
— aria-hidden="true" on all decorative icon fonts in Header.astro
and ShareNotification.astro (hamburger, search, palette, close).
— role="status" aria-live="polite" aria-atomic="true" on
ShareNotification. Screen readers now announce copy success.
— @media prefers-reduced-motion suppresses stars GIF background.
Existing .blink rule updated to add visibility:visible so cursor
doesn't vanish when animation is disabled.
— Search and theme buttons enlarged from h-7 w-7 (28px) to h-11 w-11 (44px).
Meets WCAG 2.5.5 touch target minimum.
— Guestbook honeypot: aria-hidden="true" on wrapper <p>,
tabindex="-1" and autocomplete="off" on input.
— Header greeting changed from <h1> to <p class="site-greeting">.
.site-greeting in global.css preserves text-2xl/md:text-3xl
appearance. Fixes double H1 document outline on every page.

feat: add animated stars GIF background with toggle support

March 30, 2026asce1062View on GitHubc61852a
Commit details
Adds a stars.anim.gif background to the site with a user preference
toggle. Integrates the starsBackground script into Layout, wires up
navigation and sidebar toggle UI, and adds CSS for the background layer.

fix(hooks): enforce npm test in the active pre-commit hook

March 28, 2026asce1062View on GitHub32c8943
Commit details
The pre-commit hook at the project subdirectory (asce1062/.husky/)
was never picked up. Git reads hooks from the repo root (.husky/).
Remove the dead inner hook and add npm test to the root-level hook
so tests are enforced on every commit.

refactor: update packages and migrate content config to Astro v6 format

March 28, 2026asce1062View on GitHub1515391
Commit details
  • Update to rehype-slug and rehype-extract-toc.

- Update packages.
- Move src/content/config.ts → src/content.config.ts (required by Astro v6).
- Import z from "zod" directly; astro:content no longer re-exports it.

refactor: migrate remark-slug to rehype-slug and rehype-extract-toc

March 28, 2026asce1062View on GitHub9c713b9
Commit details
Replace deprecated remark-slug with rehype-slug and migrate the TOC
extraction pipeline from the remark phase to the rehype phase.

Changes:
  • Remove: remark-slug (deprecated, no TS types)

  • Remove: @stefanprobst/remark-extract-toc

  • Add: rehype-slug (typed, maintained)

  • Add: @stefanprobst/rehype-extract-toc (has /mdx subpath, same API)


New rehypePlugins order:
rehypeSlug → extractToc → withTocExport → accessibleEmojis → externalLinks → prettyCode

The slug algorithm (github-slugger) is identical in both packages so all
existing heading anchor URLs remain valid. The maxDepth:3 option is not
available in the rehype version but TableOfContents.astro already limits
rendering to 3 levels so behaviour is unchanged.

Eliminates the remarkSlug as any cast. 0 errors on astro check, 263 tests
passing, lint and knip clean.

merge: test/security-critical-paths → main

March 28, 2026asce1062View on GitHubc886d00
Commit details
  • test/security-critical-paths:

feat(test): add security test suite and vitest infrastructure

feat(test): add security test suite and vitest infrastructure

March 28, 2026asce1062View on GitHuba5e381a
Commit details
Test infrastructure:
- vitest.config.ts - node environment, @/ path alias, globals
- package.json - test/test:watch scripts, vitest devDep
- eslint.config.mjs - vitest globals for *.test.ts files
- knip.json - vitest.config.* and test globs in entry, vitest ignored
- .husky/pre-commit - runs npm test after lint-staged

New test files:
- src/tests/middleware.test.ts - CSRF 403, token upgrade 303+cookie,
auth enforcement redirect, non-admin passthrough, security headers on all paths
- src/config/tests/email-config.test.ts - sanitizeUrl, sanitizeHeaderValue,
parseEmailAddress, sanitizeUserText, truncate, wrapText
- src/lib/api/tests/admin-auth.test.ts - isValidToken timing-safe comparison,
checkPostOrigin CSRF, checkAdminAuth token upgrade, cookie option consistency
- src/lib/api/tests/github.test.ts - renderMarkdownToHtml XSS regression
(HTML-escape ordering, javascript:/data:/vbscript: blocking), URL parsing, pagination
- src/lib/api/tests/guestbook.classify.test.ts - classifyEntry spam classifier
- src/lib/api/tests/guestbook.utils.test.ts - hashValue, safeParseReasons, assertStatus
- src/lib/api/tests/guestbook-notify.test.ts - sendEntryCopy 4 spam guards,
sendEmail retry logic (429/5xx retry, 4xx no-retry, transient errors, retry cap)

Production fixes bundled with this commit:
- src/lib/api/github.ts - renderMarkdownToHtml validates https:/http: before
injecting commit message URLs into hrefs (javascript: injection regression fix)
- src/lib/api/guestbook.ts - export assertStatus for testability; fix
OBFUSCATED_LINK_RE false-positive that auto-hid all entries containing valid URLs
- src/pages/guestbook.astro - maxlength=2000 + server-side length cap
- src/components/seo/OpenGraphTags.astro, src/pages/changelog.astro -
eslint-disable-next-line for intentional set:html usages (JSON.stringify output)

fix: update stale asset paths from public/ restructure

March 23, 2026asce1062View on GitHub37d5fb6
Commit details
  • Replace ogImage="social-preview.png" with "images/social-preview.png"

- Fix rss.xml.ts: fs.existsSync check and both channel image URLs now point
to public/images/social-preview.png
- Update privacy.astro CLI snippet: gpg --import path updated to canonical
/downloads/public.pgp

refactor: migrate to Shiki built-in Rose Pine themes and reorganize email templates

March 23, 2026asce1062View on GitHub0a69985
Commit details
  • Replace custom VS Code JSON theme exports with Shiki string

identifiers: dark "rose-pine", light "rose-pine-dawn". Removes JSON imports,
ThemeRegistration cast, and deletes public/theme/ directory entirely.
- Move email .astro templates from src/lib/email/templates/ to
src/components/email/ (standard Astro convention; lib/ is for logic only).
Update import paths in both builder files.
- Document remaining as-any casts in markdown.config.ts: remark-slug (deprecated,
no types) and rehype-accessible-emojis (upstream type gap).

refactor(public): reorganize static assets into icons/, images/, downloads/

March 23, 2026asce1062View on GitHub505c096
Commit details
Move scattered root-level assets into purposeful subdirectories:
- icons/ - all favicon + PWA + apple-touch variants
- images/ - social previews + topography SVGs
- downloads/ - resume PDF + PGP key

Remove public/css/ (icomoon CSS served from /fonts/icomoon/style.css)
and root-level icomoon font duplicates.

All legacy paths (public.pgp, resume PDF, old spaced PDF URL) redirect
301 to their new canonical locations in downloads/. Cache headers updated
from shallow globs (/*.png) to deep globs (/**/*.png) to cover subdirs,
and /**/*.svg added for topography assets.

misc(chore): visual improvements

March 23, 2026asce1062View on GitHub8d9e488

refactor(avatar): make randomize preview-only when a saved avatar exists

March 19, 2026asce1062View on GitHube1c8a62
Commit details
Randomize previously used persist: avatarStore.isRemembered(),
logically inverted from the intended behavior. Changed to
persist: !avatarStore.isRemembered() across the mini widget and
AvatarStateManager so randomize only writes to localStorage on first
use (no saved avatar yet). For returning users it is preview-only;
Save remains the explicit commit action.

Three related behavioral fixes:

- avatarMiniWidget: gender switch was also using persist: isRemembered()
and loading getDefaultState unconditionally, which could overwrite a
saved avatar of the other gender with defaults. Now mirrors
AvatarStateManager.changeGender: restores the saved avatar for the
target gender if one exists, and only persists when a saved state for
that gender is already present.

- avatarStateManager: syncStore() now accepts an optional persist override
so randomize() can bypass the auto-persist logic without affecting the
behavior of updateLayerValue() and changeGender().

- guestbookAvatarWidget: opt-in checkbox called saveToStorage()
unconditionally. A returning user who randomized then checked opt-in
would silently overwrite their saved avatar. saveToStorage() is now
guarded by !avatarStore.isRemembered().

fix: guard e.key before toLowerCase in keydown handlers

March 18, 2026asce1062View on GitHubf773526
Commit details
Autofill and autocomplete fire synthetic keydown events where e.key is
undefined, causing "Cannot read properties of undefined (reading
'toLowerCase')" in these handlers.

- keyboardShortcuts.ts: guard in registerShortcut handler
- FloatingSearch.astro: guard in Escape and Ctrl+K listeners
- themeManager.ts: guard in Ctrl+Shift+L theme toggle listener

fix(api): replace single encoded state param with separate gender+avatar params

March 18, 2026asce1062View on GitHub133defc
Commit details
Netlify's function infrastructure incorrectly re-parses percent-encoded
ampersands (%26) in query values after decoding, splitting the state
string gender=female&avatar=... into separate params and leaving state
empty causing "Missing state parameter" errors.

Fix by passing gender and avatar as direct top-level query params:
/api/avatar.png?gender=female&avatar=4-26-32-22-3-3

Neither value contains characters that need encoding, so no
double-encoding occurs and Netlify has nothing to misparse.

- avatar.png.ts: read gender + avatar directly; reconstruct state
string internally for compositeAvatarPng
- notify-template.ts, entry-copy-template.ts: parse avatarState with
URLSearchParams and build the new flat URL format

fix(avatar): mark /api/avatar.png as prerender = false

Astro was prerendering the API route at build time with no query params,
storing the 400 error body ("Missing gender or avatar parameter") as a
34-byte static file at dist/api/avatar.png. Netlify serves static files
before routing to functions, so every request hit that file and never
reached the SSR function.

fix(pwa): exclude API routes from SW navigation fallback and CDN caching

March 18, 2026asce1062View on GitHubcf1f930
Commit details
astro.config.mjs:
- Add /^\/api\// to navigateFallbackDenylist so API navigation
requests always reach the network
- Add NetworkOnly runtimeCaching rule for /^\/api\// so API responses
are never stored in the SW cache

netlify.toml:
- Add Cache-Control: no-store for /api/* to prevent Netlify's CDN
from treating /api/avatar.png as a static PNG asset (the /*.png
glob rule was matching it)

fix(avatar): respect saved avatar when switching gender

March 18, 2026asce1062View on GitHubfc52d05
Commit details
changeGender() always called getDefaultState(), discarding any saved
avatar regardless of localStorage. Now checks for a persisted avatar
matching the target gender first, falling back to defaults only when
none exists.

Adds avatarStore.getSavedStateForGender(gender) which reads directly
from localStorage so the check is always against the persisted value,
not the current in-memory store state.

fix(contributions): clip iframe white body bleed via overflow-y-hidden

March 18, 2026asce1062View on GitHub7a3bcf3
Commit details
The jandee iframe renders its graph content at ~162px with the
remaining body background bleeding white at the bottom. Since the
iframe is cross-origin, CSS cannot be injected to fix it directly.

Replace the mask div approach (which scrolled away on horizontal
overflow) with a clip: container is h-[162px] with overflow-y-hidden,
iframe is h-48 (192px). The bleed sits in the clipped region
and never renders. Horizontal scrolling is unaffected.

feat: avatar system, match-device theme, feedback manager hardening, and privacy updates

March 18, 2026asce1062View on GitHubdd33402
Commit details
Avatar system
- Add AvatarMiniWidget component and avatarMiniWidget.ts with render race
guard renderSeq, duplicate binding guard data-widget-bound, and
AbortController-based lifecycle cleanup
- Add avatarStore.ts: singleton state bus with CustomEvent dispatch,
localStorage persistence via getPref/setPref, and cross-tab clear support
- Add avatarRenderCore.ts: shared canvas compositing (Promise.all concurrent
layer loading, zIndex ordering), state serialization/deserialization with
strict /^\d+$/ + Number.isFinite + Number.isInteger validation
- Add entryAvatarRenderer.ts: renders pixel art stamps on guestbook entry
canvases from data-avatar-state attributes
- Add guestbookAvatarWidget.ts: guestbook avatar opt-in flow with
AbortController lifecycle, hasDeliberateChoice guard, submitFallbackApplied
double-submit guard, and avatar state injection into form submission
- Add src/pages/api/avatar.png.ts: server-side PNG avatar compositing
endpoint with MAX_STATE_LENGTH=256 guard, duplicate param rejection,
and HTTP 400/500 semantics
- Add src/lib/email/helpers/avatarImage.ts: sharp-based PNG compositor
returning CompositeResult discriminated union; layer asset errors include
{ cause: err } for full stack chain
- Update email templates (EntryCopyEmail, NotifyEmail, builders) to embed
avatar image via /api/avatar.png endpoint instead of data URI
- Add avatar-frame.css utility and AvatarActions component
- Update GuestbookEntry, GuestBookEntryActions, AvatarGenerator, Sidebar
to wire avatar mini widget and opt-in flow

Theme system
- Add matchDeviceTheme.ts: sidebar "Match device" toggle wired to OS
prefers-color-scheme with AbortController cleanup, URL ?theme= override
precedence, and astro:after-swap re-stamp
- Harden themeManager.ts: move isMac detection into setupThemeShortcut,
fix updateThemeIcon to remove both classes before adding, add editable
field guard (INPUT/TEXTAREA/SELECT/contenteditable) to keyboard shortcut,
tighten getCurrentTheme/resolveActiveTheme JSDoc

Feedback manager
- Rename shareManager.ts → feedbackManager.ts
- Add notificationHideTimers map to track and cancel in-flight fade-out
completion timers independently from auto-hide timers
- Add iconRestoreTimers WeakMap for per-element flashCheckIcon restore timer
cancellation; add primaryIcon === checkIcon same-element guard
- Guard navigator.clipboard?.writeText availability in copyToClipboard
- Extract FADE_OUT_DURATION = 300 constant
- Replace clone-and-replace listener pattern with data-feedback-bound
dataset guard in initShareNotification
- Add notification.isConnected check before deferred DOM mutations

Preferences and privacy
- Add match-device-theme and avatar-state keys to PREF_KEYS
- Update PrivacyPolicy.astro, privacy.astro, and
public/.well-known/dnt-policy.txt: declare all four localStorage keys
(theme, match-device-theme, cursor-blink, avatar-state) with descriptions;
dnt-policy.txt reassertion date bumped to 2026-03-18

Miscellaneous
- Rename "Alex Mbugua Ngugi - Resume.pdf" → "Alex-Mbugua-Ngugi-Resume.pdf"
(remove spaces from public asset path)
- Add new icomoon icons to font set
- Split CSS utilities; add avatar-frame, guestbook, sidebar style updates
- Update db/config.ts and db/seed.ts
- Update guestbook.astro, ProjectCard, Skills, ThemeSwitcher, AsciiWidget,
SocialShareButtons, navigation, and Layout for new features
- Update blog/notes content

refactor(toc): rewrite scroll behavior, sticky detection, dead zone, and rAF batching

March 13, 2026asce1062View on GitHubd934510
Commit details
Replace brittle TOP_ZONE_PX constant with live getBoundingClientRect().top
check so sticky state is detected accurately regardless of post layout,
preview image presence, or lazy-loaded content.

- SCROLL_THRESHOLD raised 10px → 50px: prevents direction reversal within
the 300ms transition window, eliminating the visible bump on touchpad momentum
- setHidden() state guard: only writes DOM transform on actual state change,
eliminating redundant writes that re-evaluate the CSS transition
- rAF batching: burst scroll events collapse into one DOM write per frame
- isDetailsOpen tracked via native toggle event, not re-queried per scroll tick
- AbortController lifecycle: all listeners tied to one signal, cleanly torn
down on Astro soft navigation re-init
- lastScrollY only advances when a direction decision is made. Sub-threshold
oscillations accumulate toward threshold rather than endlessly resetting it
- TableOfContents.astro: single astro:page-load listener replaces double-init

chore: remove unused navigation exports and prune stale knip ignores

- Remove SocialLink interface, socialLinks, and contactLinks. Dead code
since footer links were consolidated into the sidebar
- Drop netlify-plugin-checklinks and netlify-plugin-submit-sitemap from
knip.json ignoreDependencies (plugins no longer in use)

feat: preferences system, CSS utilities split, privacy page redesign

March 13, 2026asce1062View on GitHubc654acb
Commit details
Preferences & storage
- add src/lib/prefs.ts
- centralized localStorage helpers (getPref/setPref/removePref) and PREF_KEYS constants
- update Layout.astro
- inline script to inject cursor-blink key via define:vars (single source of truth)
- update sessionStorage → localStorage across PrivacyPolicy.astro, dnt-policy.txt

Cursor blink preference
- new cursorBlink.ts
- localStorage persistence
- astro:after-swap flash prevention
- cross-tab sync via storage event
- add data-driven sidebar Options sidebarOptions[] section to navigation.ts
- add SidebarOption interface with icon + label + description fields

Theme manager
- fix getCurrentTheme priority: URL > localStorage > data-theme > default
- isMac computed once at module level (not per keydown)
- setupThemeShortcut now takes AbortSignal instead of returning cleanup fn
- ThemeSwitcher.astro simplified to single astro:page-load + AbortController

CSS utilities
- split global.css into src/styles/utilities/ (btn, badge, input, checkbox, toggle, link-action, content)
- add color variants (8 semantic colors) and size variants to all utilities
- fix toggle + checkbox base color to use base-content, not primary
- scope markdown.css checkbox rules to li to prevent bleed into sidebar toggle

Privacy page
- full redesign. Updated content and structure

feat: sidebar navigation, ASCII widget system, new pages, and site-wide polish

March 12, 2026asce1062View on GitHub3568fa1
Commit details
Sidebar
- New Sidebar.astro component with CSS checkbox open/close state machine
- Collapsible nav drawer: hamburger, overlay click-trap, scroll lock on mobile
- Always-visible on desktop lg:translate-x-0, slide-in on mobile
- Nav links with active state highlighting and icon tilt on hover
- Avatar footer with controls sidebarAvatar.ts
- ASCII easter egg section: random art on load, dice randomize, brand name
badge updates to match the art's text value on dice click sidebarEgg.ts
- sidebarNav.ts: keyboard nav, AbortController cleanup across soft-navs

ASCII widget system
- AsciiWidget.astro: shared component with dice button, clipboard copy button,
font label, and art <pre>, all namespaced under a widgetId prop
- asciiReveal.ts: phosphor "wake-up" animation via IntersectionObserver.
Plays on viewport enter, resets on exit for re-animation on scroll-back;
replayOnDice option for 404 page; reduced-motion stable state; full teardown
(AbortController, observer disconnect, timer clear) for Astro soft-nav safety
- asciiWidget.ts: centralized init, variant parsing, no-repeat random pick,
render (art + font label + data-ascii-current), dice wiring, reveal setup;
onRender callback for side effects; used by sidebar, about, and 404
- Copy button: outputs [Font] header + ``figlet visual art block +
`json fenced valid JSON with properly escaped \n. one human-readable
and one programmatically parseable
-
ascii-art.json: pre-generated variant data
-
scripts/generate-ascii.mjs: generation script for ascii-art.json
-
asciiReveal.css, asciiWidget.css`: component stylesheets

New pages
- /about: whoami write-up + neofetch-style system profile with ASCII art
- /hello: contact / say hello page
- /interests: interests and media diet
- /verify: PGP / identity verification

Page and layout updates
- 404: AsciiWidget with phosphor reveal and replayOnDice
- Header, Layout: sidebar integration and structural adjustments
- changelog, colophon, now, palette: copy and layout refinements
- navigation.ts, site-config.ts, site-utils.ts: new routes and config

Scripts
- consoleEgg.ts: console easter egg
- neofetch.ts: sidebar neofetch display

Styles
- sidebar.css: nav drawer, overlay, easter egg flicker animation
- markdown.css: prose content component styles
- global.css, theme.css: updated imports and token updates

Content
- New notes: guiding-principles, licensing
- Updated personal site checklist to reflect current build state

Icons and tooling
- Updated icomoon icon set
- Updated pubvendors.json and added it to .well-known
- eslint and prettier config updates

feat: email notifications, admin moderation hub, security hardening, dead code cleanup

March 5, 2026asce1062View on GitHub2a15279
Commit details
A broad feature + housekeeping pass spanning the guestbook subsystem,
admin infrastructure, email delivery, and project tooling.

EMAIL NOTIFICATIONS
src/lib/email/
src/lib/api/guestbook-notify.ts
src/config/email-config.ts

Integrate Resend for transactional email on new guestbook submissions:

Owner notification NotifyEmail.astro
- Fires on every new entry that passes spam checks.
- Includes submitter name, message, website/email if
provided, submission timestamp, and a direct link to the admin moderation
queue for one-click review.

Sender copy EntryCopyEmail.astro
- Opt-in confirmation sent to the submitter when they provide an email address.
- Acknowledges receipt and sets expectations about moderation.

Both templates are full HTML with light/dark theme variants keyed to the
submitter's active theme at time of submission. Plain-text fallbacks
included for clients that strip HTML.

Delivery orchestration in guestbook-notify.ts
- Handles both sends in parallel.
- Swallows per-send errors to keep them non-blocking.
- Logs outcomes without leaking PII to the console.

email-config.ts
- centralizes From/Reply-To addresses, subject-line
templates, and the Resend API key reference in one place.

ADMIN SECTION
src/pages/admin/index.astro
src/pages/admin/guestbook.astro

/admin
- token-based login screen.
- On success sets a 7-day httpOnly cookie and redirects
to the hub.
- Wrong token triggers a 500 ms server-side delay to slow
sequential brute-force attempts, then shows an inline error.
- Authenticated view lists all protected pages.

/admin/guestbook
- full moderation interface with four filter tabs:
- All
- Pending
- Hidden
- Audit Trail

Entry cards display:
- numeric #ID
- submitter name + URL + email
- current status badge warning/error/success
- moderation score
- flag reasons as inline pills
- message body in a scrollable pre block
- submission timestamp
- moderator/moderated-at metadata when available

Actions per entry:
Approve - set status → visible keep existing flags
Approve & Clear - set status → visible AND wipe flag reasons/score
for false positives
Hide - set status → hidden

Audit Trail tab renders the full GuestbookModerationLog as a compact
monospace table:
- entry deep-link
- action badge
- status transition from → to
- flag reasons at time of action
- score
- actor
- timestamp
Entry ID cells link back to the entry card in the All view via anchor.

CENTRALIZED ADMIN AUTH
src/lib/api/admin-auth.ts

sanitizeToken()
- strips CR/LF copy-paste artifact guard
- trims whitespace
- caps at 256 chars (arbitrary sanity limit to prevent DoS attempts with huge input)

safeEqual()
- wraps node:crypto timingSafeEqual.
- Performs a dummy same-length comparison when lengths differ
so the function always runs in proportional time regardless of input,
preventing length-based timing leaks.

isValidToken()
- reads ADMIN_TOKEN from env, guards against unset env var,
delegates to safeEqual via sanitizeToken on both sides.

setAdminCookie() / deleteAdminCookie()
- consistent cookie options on both set and delete
(path:/admin, httpOnly, secure:prod-only, sameSite:strict, maxAge:7d)
to prevent orphaned cookies.

checkPostOrigin()
- validates Origin header.
- Falls back to Referer for older browsers
- Explicitly rejects the string "null" sent by sandboxed iframes.

checkAdminAuth()
- unified auth check for cookie or ?token= query param
- returns cleanUrl token stripped so callers can redirect immediately,
keeping the token out of browser history and server logs.

MIDDLEWARE
src/middleware.ts

Centralize cross-cutting admin concerns so individual pages stay lean:

Route matching
- pathname === "/admin" || pathname.startsWith("/admin/")
- the trailing-slash prefix prevents false matches on /administer etc.

CSRF guard
- every admin POST is checked via checkPostOrigin before any
page logic runs.
- returns 403 Forbidden on failure.

?token= upgrade
- on any admin route, a valid token in the query string
causes the middleware to set the cookie and 303-redirect to the clean URL
before the page ever renders, keeping tokens out of browser history,
access logs, and Referer headers.

Auth enforcement
- /admin/* sub-pages redirect to /admin if auth fails.
- The /admin hub itself handles its own login form.

applyAdminHeaders()
- applied to every return path, including the early
CSRF 403 and auth redirects so no response ever escapes without the
full security header set:
      Content-Security-Policy: frame-ancestors 'none'; base-uri 'none'; form-action 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Cache-Control: no-store, max-age=0 + Pragma: no-cache + Expires: 0
Referrer-Policy: no-referrer
X-Robots-Tag: noindex, nofollow, noarchive
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Strict-Transport-Security: max-age=31536000 (production only)


DATABASE & GUESTBOOK API
db/config.ts
src/lib/api/guestbook.ts

Add GuestbookModerationLog table to persist a full audit trail of every moderation action.
- entryId
- action
- fromStatus
- toStatus
- reasonsBefore
- scoreBefore
- actor
- at

getModerationLog(entryId?: number)
- no arg returns the full log for the audit tab.
- with entryId returns per-entry history for future detail views.

updateEntryStatus()
- gains a clearFlags option that zeroes out
moderationReason and moderationScore when approving false positives.

safeParseReasons()
- helper for safely deserializing the JSON reasons array
stored in the DB, with a graceful fallback to [].

Seed data expanded with styled sample entries covering the full range of
status/theme/pattern combinations for visual QA.

DATE FORMATTING
src/lib/content/utils.ts

Extend formatDate(date, format) with a "medium" variant (month: "short"
→ "Jan 1, 2024") to sit between "long" (January 1, 2024) and "short"
(2024-01-01).

Remove formatEntryDate() from guestbook.ts and formatChangelogDate()
from github.ts. Both were private wrappers around the same locale
options. All consumers updated to call formatDate(..., "medium") or
formatDate(...) directly.

DEAD CODE CLEANUP
src/config/
src/lib/api/github.ts

Delete src/config/index.ts
- re-export barrel with zero consumers.

Remove getAbsoluteUrl() from site-utils.ts
- no call sites in the codebase.
- URL construction handled inline or via Astro.url.

Remove groupEntriesByYear() from github.ts
- changelog.astro never imported it.
- entries are grouped directly in the template.

Remove export from hexColorValues in design-tokens.ts
- used only internally to derive hexLight/hexDark, never consumed outside the module.

PGP / SECURITY FILES
public/
public/.well-known/

Rotate public PGP key
- public/public.pgp
- public/.well-known/security.txt
- public/security.txt
- public/.well-known/security.txt.sig

Add WKD (Web Key Directory) support
- openpgpkey/hu/ hash files + policy
- so mail clients can auto-discover the key from the domain
when users click the email address in the guestbook entry.

Update privacy.txt and dnt-policy.txt content.

TOOLING
knip.json
eslint.config.mjs
package.json

knip.json
- add ignoreExportsUsedInFile for interface/type/function
- expand entry patterns to cover scripts/ and db/ directories
- add ignoreIssues for email template Astro files (false-positive type exports).

eslint.config.mjs
- sync rule updates.

package.json`
- add resend and validator.js
- update dependencies.

feat: add guestbook entry action menu and improve cross-browser background pattern support

February 20, 2026asce1062View on GitHub47beb5e
Commit details
Add interactive kebab menu to each guestbook entry card
Start with 5 actions:
- Copy Style: copies entry's theme JSON to clipboard
- Request Edit/Remove: pre-filled GitHub issue (new tab)
- Request via Email: pre-filled mailto link for non-GitHub users
- Copy Message: copies raw message text to clipboard
- Share Entry: Web Share API with permalink clipboard fallback

Entry anchor navigation: #entry-{id} URLs scroll to and highlight the
target entry with an accent outline pulse animation.

Cross-browser pattern backgrounds
- Replace -webkit-mask-image with progressive enhancement
- Fallback uses background-image with:
- Reduced opacity for unsupported browsers
- @supports (mask-image: none) upgrades to the full themed mask effect

Other changes:
- EntryActions.astro: new component with menu trigger and action items
- GuestbookEntry.astro: integrate EntryActions, add id="entry-{id}"
- guestbook.astro: wire up initEntryActions + ShareNotification
- guestbook.css: action menu styles, z-index stacking, highlight keyframes
- eslint.config.mjs: add DOMException to browser globals
- knip.json: ignore guestbookEntryActions.ts
- db/seed.ts: update seed data
- IcoMoon: update icons

feat: refine guestbook customizer UI and entry card layout

February 20, 2026asce1062View on GitHub3058e53
Commit details
Customizer:
- Add base-100, base-200 to color swatch selection
- Change color swatches from circles to squares (border-radius: 0rem)
- Extract swatchColors config, shared across border color pickers
- Set pattern-swatch border-radius to 0rem for visual consistency

Entry cards:
- Move author name + date to bottom of card, right-aligned
- Stack name and date vertically
- flex-direction: column
- Rename .entry-header → .entry-meta
- Add neutral background pill to entry-meta for legibility over some patterns
- Change date format from en-gb (day month year) → en-us (month day, year)
- Add topography.svg to public/notecards/
- Default bg to "topography" pattern for all entries

feat: add guestbook entry theme customizer with Hero Pattern backgrounds

February 19, 2026asce1062View on GitHub92b3a02
Commit details
Allow users to personalize their guestbook entry card appearance.
Selections are stored as a JSON style column in the DB.
These are then rendered as inline styles on each entry card.

Schema:
style column stores a JSON string with the following shape:
    {
"bg": "topography", // Hero Pattern filename or "none"
"borderColor": "primary", // Semantic color: base-300 | primary | secondary | accent | info | success | warning | error
"borderWidth": "1.5px", // 0.5px – 4px in 0.5px steps
"borderStyle": "dashed", // solid | dashed | dotted | double | none
"borderRadius": "0.5rem" // 0rem | 0.25rem | 0.5rem | 1rem | 2rem
}

Entries with style: null render with defaults (bg-neutral, no pattern, base-300 border, 1px solid, 0.25rem radius).

Changes:
- Add style column (optional text) to Guestbook DB schema
- Add Hero Pattern SVGs to public/notecards/
- Source/Credits: Steve Schoger
- Theme-aware rendering via CSS mask-image (light/dark fill colors)
- Add customizer UI:
- Add pattern dropdown, border radius/width/color/style
- Add pickers with top-right corner previews
- Add live textarea preview
- Add randomize button
- Add client-side customizer script with hover preview on textarea,
click-outside-to-close, and astro:page-load support for View Transitions
- Refactor Expand component from CSS checkbox hack to JS toggle
- Extract customizer constants to src/config/guestbook.ts
- Rename Card.astro to ProjectCard.astro

feat: migrate guestbook from Netlify Forms to Astro DB + Turso

February 18, 2026asce1062View on GitHub32e011c
Commit details
Replace Netlify Forms with Astro DB backed by
Turso (libSQL) for full data ownership and host-agnostic deployment.

- Add SSR guestbook route with inline POST handling and redirect flow
- Implement honeypot + content pattern spam detection
- HTML tags, BBCode, URL shorteners, obfuscated links, link-only posts
- Preserve ASCII art in messages with <pre> rendering and whitespace-
safe input handling
- Add Netlify adapter (conditional, production-only) with imageCDN off
- Add @astrojs/ts-plugin for virtual module type resolution
- Create CSV migration script (papaparse) for Netlify Forms export
- Update privacy policy, colophon, DNT policy, and pubvendors
- Reflect Turso-backed storage with no third-party data processing
- Clean up knip config and tsconfig for db/ directory

refactor: replace remark-reading-time with Astro recipe, add external link icons

February 18, 2026asce1062View on GitHub2b412de
Commit details
  • Replace remark-reading-time + bridge plugin with a custom remark plugin

- Use reading-time and mdast-util-to-string directly

- Add rehype-external-links
- Automatically append icon-box-arrow-up-right to external links in content collections
- With target="_blank" and rel="noopener noreferrer" for security

- Update knip.json schema to @latest and clean up stale ignore entries.

feat: add notes collection, unified content architecture, privacy/well-known infrastructure

February 17, 2026asce1062View on GitHubd489a1c
Commit details
Notes & Content Architecture
- Add Notes collection with schema, utilities, and listing page (/notes)
- Merge BlogLayout + NoteLayout into unified ContentLayout with collection prop driving blog vs notes behavior
- Merge Post + NoteCard into unified ContentCard
— image prop controls layout
- flex-row with image for blog, column for notes
- Merge blog/notes utilities into shared src/lib/content/utils.ts
- formatDate, getContentUrl, sortByDate, getAllTags
- Extract common Zod contentSchema with shared fields
- title, description, pubDate, updatedDate, tags, permalink, draft
- blogSchema extends with image/featured, notesSchema currently uses base as-is
- Rename src/components/blog/ → src/components/content/ for cross-collection reusability
- Generalize PostNavigation → ContentNavigation with basePath prop and generic "Previous"/"Next" labels
- Update all blog content MDX files from BlogLayout → ContentLayout
- Add notes: hello-notebook and personal-site-checklist
- Add Notebook and Privacy to header navigation
- Add new blog post: from-necessary-to-impossible

Reading Time & Word Count
- Integrate remark-reading-time npm package for AST-based reading time calculation
- Add bridge plugin (remarkReadingTimeToFrontmatter) to map file.data.readingTime → file.data.astro.frontmatter
- remark-reading-time writes to file.data but Astro exposes file.data.astro.frontmatter
- Remove custom stripMarkup/estimateReadingTime/countWords
— remark plugin walks text+code AST nodes, inherently ignoring JSX props, imports, frontmatter, and HTML attributes
- Listing pages pre-render entries via render() from astro:content to access remarkPluginFrontmatter

Privacy & Well-Known Infrastructure
- Add /privacy page with PrivacyPolicy component, DNT, Security (with WKD note), Vendors, and Well-Known Resources sections
- Add .well-known/dnt
— JSON tracking status ("N" Not Tracking) per W3C DNT spec
- Add .well-known/dnt-policy.txt
— comprehensive DNT compliance policy with operator info, scope, data collection, forms, server logs
- Add .well-known/security.txt + /security.txt
— RFC 9116 security contact with no-consent-for-testing notice
- Add .well-known/security.txt.sig
— detached PGP signature
- Add .well-known/pubvendors.json
— Netlify, GitHub, jandee (Vercel), Google, IcoMoon, Pagefind
- Add .well-known/change-password
- Generate PGP key (RSA 4096, expires 2028-02-16) and publish at /public.pgp
- Add WKD notes to security.txt, dnt-policy.txt, and privacy page indicating future ping@alexmbugua.me migration
- Update netlify.toml
- Add /.well-known to skipPatterns in netlify.toml
- since there's no index page at that path (it's just a directory of machine-readable files)

Styling & Fixes
- Add site-wide checkbox styling in global.css using icomoon font glyphs (\e9aa unchecked, \f0ed checked)
- Fix circular dependency between site-config.ts and site-utils.ts — helpers now imported directly from site-utils
- Add updatedDate with future-date validation to content schema
- Harden offline fallback and PWA resource interception
- Add retry button and periodic server polling (2.5s) to the offline page
- The browser online event is unreliable on captive portals, so we verify with a real HEAD request.
- Extend navigateFallbackDenylist to exclude
- .json, .pgp, .sig, and /.well-known/ paths
- Ensures the service worker no longer redirects machine-readable resources to the offline page.
- PGP keys, dnt policy, etc.

fix: default ogImage to social-preview.png in Layout

February 5, 2026asce1062View on GitHub50307e1
Commit details
Use SEO.ogImage as fallback when ogImage prop is not provided,
preventing blank social preview images on pages without explicit ogImage.

feat: add changelog page with GitHub commit history

February 4, 2026asce1062View on GitHubb1cb699
Commit details
  • Add /changelog page displaying all commits fetched at build time

- Extend src/lib/api/github.ts with changelog functions:
- fetchAllGitHubCommits() with pagination support
- getChangelogEntries(), formatChangelogDate(), renderMarkdownToHtml()
- getRepoSlugFromUrl(), getBaseRepoUrl() helpers
- Features:
- Shows 25 commits initially with "Load more" button
- Year grouping with sticky headers
- Expandable commit details for multi-line messages
- Graceful error handling for API failures
- Uses GITHUB_PAT for higher rate limits (5000/hr vs 60/hr)
- Add changelog link to navigation and colophon page

feat: add palette page with design tokens and click-to-copy

February 4, 2026asce1062View on GitHubca5d9e1
Commit details
  • Add /palette page with tabbed Swatches/Ingredients views

- Create design-tokens.ts registry with colorRoles and colorValues
- Implement CSS-only tab switching with accessibility (ARIA roles)
- Add click-to-copy for computed colors and OKLCH values
- Add palette to header navigation between Colophon and Meta
- Update icon font with palette icons
- Link palette from colophon Typography & Design section

feat: add colophon/meta pages, refactor config to grouped exports

February 3, 2026asce1062View on GitHub2ec25be
Commit details
Add colophon/meta pages:
- /meta: diagnostic, minimal, noindex (build info, package versions, SEO defaults)
- /colophon: narrative credits, philosophy, tech stack with icons

Config architecture improvements:
- Create deps-info.ts (centralized package.json access)
- Create build-info.ts (build timestamp, Node version)
- Create site-utils.ts (helper functions)
- Create index.ts barrel export
- Remove all legacy individual exports from site-config.ts
- Migrate all consumers to grouped exports (SITE, BLOG, SOCIAL, SEO, PWA, etc.)

SEO/meta tag fixes:
- OpenGraphTags: WebPage schema for non-articles, stable WebSite name,
Twitter meta uses name attr, title-case breadcrumbs, add isPartOf
- MetaTags: remove fake Atom feed, inaccurate geo metadata, obsolete tags
- Layout: safe Astro.site fallback, fix duplicate id on main element

Misc:
- Add /humans.txt following humanstxt.org standard
- Add .txt to service worker navigateFallbackDenylist
- Add Colophon and Meta to site navigation

Housekeeping:
- Icon font updates
- Remove trailing design-tokens.mjs and tailwind.config.mjs since tailwindCSS v4 update

feat: enhance project cards, theming, and navigation UX

January 29, 2026asce1062View on GitHub7cc0187
Commit details
Project Cards:
- Add contributors, issues, and forks counts with icons
- Fetch contributor count via GitHub API
- Conditional color styling (warning for 0, info for >= 1)
- Separate stats row from project name link

Theme System:
- Add URL parameter support (?theme=dark or ?theme=light)
- Add keyboard shortcut (Ctrl/Cmd + Shift + L) for theme toggle
- Export setTheme() for programmatic theme control

Navigation & Footer:
- Replace inline SVG with icon-git-commit in footer
- Add hover pause for blink animation on nav links

feat: update 0xProto font, sticky TOC, and code block styling

January 23, 2026asce1062View on GitHub7052393
Commit details
  • Update 0xProto font to complete set (Regular, Bold, Italic) with

woff2, otf, ttf formats; remove old NerdFont version
- Remove unused Josefin Sans and Poppins fonts
- Add sticky table of contents that shows on scroll-up only,
with auto-close on link click
- Add scroll-margin-top to headings for proper anchor positioning
- Set heading font weights (h1-h3 bold, h4-h6 semibold)
- Style .btn and .link-action with 0xProto italic
- Italicize footer build info text
- Update rehype-pretty-code config with grid option for full-width
line highlighting
- Fix code block theme switching to use data-theme attribute
- Add border and improved styling to code blocks
- Fix resume page fallback to fit content instead of fixed height

feat: upgrade to Tailwind CSS v4 with CSS-first configuration

January 14, 2026asce1062View on GitHube301759
Commit details
BREAKING CHANGE: Tailwind CSS v3 → v4 migration

Core Changes
- Replace @tailwind directives with @import "tailwindcss"
- Migrate to @tailwindcss/postcss plugin
- Convert theme config to CSS-first approach with @theme directive
- Replace custom utilities with @utility directive syntax

Component Refactoring
- Extract CollapsibleNav as reusable component with:
- Height-based animation
- Full keyboard accessibility (Tab, Enter, Space)
- ARIA attributes for screen readers
- Smooth transitions with reduced-motion support

Bug Fixes
- Remove FOUC prevention that caused blank pages in tailwind v4
- Fix Card component flex layout with flex! override
- Fix breadcrumb inline wrapping with inline! override
- Fix nav text color inheritance from parent link
- Remove unused collapsibleNav.ts script

## Config Updates
- Update postcss.config.mjs for @tailwindcss/postcss
- Add autoprefixer and postcss-nesting to knip ignore list

refactor: more theming updates and migrations pre tailwindcss v4 upgrade

January 14, 2026asce1062View on GitHub2a498a4
Commit details
Finalize the migration to a semantic CSS variable system that eliminates
light/dark theme duplication across the codebase.

Key changes:
- Fix CSS variable references in global.css to use correct theme paths
- Update tailwind.config.mjs to expose design tokens for theme() function
- Refactor entire codebase to use theme aware semantic variables
- Implement transparency support via color-mix() in project and blog cards
- Unify behavior for buttons and action links

chore: migrate from @astrojs/tailwind to PostCSS + Tailwind v3

January 9, 2026asce1062View on GitHub18bbbc9

feat: establish retro design token system and theme infrastructure

January 6, 2026asce1062View on GitHub114188a
Commit details
Major overhaul of the color system:
- Support semantic theme colors (brand, neutral, secondary, accent)
- Support backward compatibility with palette/light numbered scales

Design Token Updates:
- Expand design-tokens.mjs with full light/dark theme color scales
- Add documentation with usage patterns and examples
- Include semantic colors (brand-primary, default-font, subtext-color, etc.)
- Maintain backward compatibility with palette/light numbered scales

Theme Configuration:
- Update tailwind.config.mjs to expose new semantic color utilities
- Add theme.css for Tailwind CSS v4 compatibility

Component & Config Updates:
- Fix guestbook textarea placeholder visibility issue
- Update Prettier config htmlWhitespaceSensitivity setting to respect whitespace properly
- "ignore" to "strict" to preserve whitespaces as written
- Refactor components to use new design token references
- Update topography SVG variants for light/dark themes

This commit establishes the foundation for migrating away from legacy palette/light
numbered scales to a more maintainable semantic color system. These changes maintain
backward compatibility while preparing for future migration.

2025

update: footer resume icon

December 31, 2025asce1062View on GitHubefce735

fix: prevent service worker from intercepting PDF navigation on mobile

December 31, 2025asce1062View on GitHub6e67114
Commit details
  • Fix issue where clicking "Open Resume" fallback on mobile showed offline page

- Add /\.pdf$/ to navigateFallbackDenylist in astro.config.mjs

fix: global search focus when not in view

December 31, 2025asce1062View on GitHub74ac388

feat: improve PDF resume viewer with browser detection

December 31, 2025asce1062View on GitHub53b52e2
Commit details
  • Add fallback overlay for browsers without inline PDF support

- Implement navigator.pdfViewerEnabled API for PDF detection
- Add iOS and in-app browser detection
- Show download button and text version link when PDF can't render inline
- Hide text version link when fallback overlay is displayed
- Update CSP headers in netlify.toml to allow PDF iframe loading
- Add 'self' to frame-src directive
- Change frame-ancestors from 'none' to 'self'
- Update X-Frame-Options from DENY to SAMEORIGIN

Fixes issues where:
- Some browsers showed blank iframes
- Some browsers displayed "contact site owner" errors

Browser support:
- Desktop: Shows inline PDF viewer (all modern browsers)
- iOS/Mobile: Shows fallback with download button immediately
- In-app browsers: Shows fallback automatically
- Legacy browsers: Falls back based on mobile detection

fix: update CSP frame-src directive to allow self

December 30, 2025asce1062View on GitHub91d704d

feat: add floating search modal with responsive positioning

December 30, 2025asce1062View on GitHubed1da27
Commit details
Add a global floating search modal that can be triggered from anywhere on
the site via the search icon or Ctrl+K/Cmd+K keyboard shortcut.

- Create FloatingSearch.astro component with dynamic positioning
- Positions at first HR divider in header
- Width matches body container responsively across all viewports
- Uses separate pagefind instance to avoid conflicts with search page
- Hidden on /search page to prevent duplication

- Extract reusable SearchBox.astro component
- Shared by both search page and floating modal
- Accepts searchId prop for unique pagefind instances

- Add search trigger button to Header
- Positioned below theme switcher toggle
- Only visible when not on search page

- Update search page to use SearchBox component
- Maintains existing functionality with new architecture
- Uses dedicated page-searchbox instance

- Update search.css to support both search contexts
- Styles apply to #pagefind-search and #pagefind-search-floating
- Consistent appearance across page and modal

Technical improvements:
- TypeScript null safety fixes in FloatingSearch
- ResizeObserver for responsive width updates
- Debounced resize handlers for performance

feat: Add resume viewer page and UI enhancements

December 30, 2025asce1062View on GitHubcdfe807
Commit details
  • Create dedicated Resume page with PDF iframe viewer (/resume)

- Implement reusable iframe theme management system (src/scripts/iframeTheme.ts)
- Enhance header breadcrumb navigation
- Add "/alex/" to path structure (~/ home / alex /)
- Implement grouped hover effect for home/alex links
- Improve UI
- Make BackToTop button position responsive (bottom-4 md:bottom-8)
- Replace footer commit text with Git commit SVG icon
- Remove rounded corners site-wide for sharper design

updates: bold header, fix footer contact information first row alignment

December 29, 2025asce1062View on GitHub47cb447

updates: add terraform icon

November 4, 2025asce1062View on GitHub5a5767a

updates

October 28, 2025asce1062View on GitHub42f60d0

updates:

October 27, 2025asce1062View on GitHub45c14ee
Commit details
  • fix header nav items rotation

- add space before posts

fix: resolve unparsable JSON-LD structured data in search console

October 27, 2025asce1062View on GitHub350cc59
Commit details
  • Fix JSON-LD schema parsing errors reported by Google Search Console

- Change from set:text to set:html for JSON-LD script tags to prevent HTML escaping
- Add is:inline directive to all JSON-LD scripts to silence Astro warnings
- Add security documentation explaining safe use of set:html with JSON.stringify()
- JSON.stringify() automatically escapes dangerous characters (< > & " ')
- All data comes from controlled sources (MDX frontmatter, config files)
- No user input from forms or URL parameters
- Disable astro/no-set-html-directive ESLint rule at file level with justification
- Added a helper function formatBreadcrumbName() that:
- Returns "Blog" if the segment is "blog"
- Removes the date prefix matching the pattern YYYY-MM-DD- (e.g., 2025-08-13-)
- Converts remaining hyphens to spaces
The issue was that set:text was HTML-escaping quotes to &quot;, making the
JSON unparsable. Using set:html with JSON.stringify() is safe because:
1. JSON.stringify() escapes all dangerous characters
2. We control all data sources (no user input)
3. Characters like < and > are converted to \u003c and \u003e

Fixes Google Search Console error:
"Parsing error: Missing '}' or object member name"

feat: implement collapsible navigation with dynamic row detection

October 24, 2025asce1062View on GitHubcbff541
Commit details
  • Add chevron-bar-down icon to icomoon font set

- Create collapsibleNav.ts utility for responsive navigation management
- Dynamically detects first row items based on viewport width
- Automatically shows/hides toggle button when items wrap
- Smooth 180° rotation animation on expand/collapse
- Update Header component with collapsible navigation
- Add nav-toggle button with chevron icon
- Use Tailwind transition-transform for smooth animations
- Center toggle button on all screen sizes
- Maintain min-height to prevent layout shift
- Update navigation layout for better responsive behavior
- Change from center to left alignment on desktop (sm:justify-start)
- Tighten spacing from gap-1 to gap-0.5
- Reduce padding: px-1.5 (mobile), sm:px-1 (desktop)
- Improve horizontal rule visibility
- Change from bg-palette-700 to bg-palette-900
- Add border-0 to prevent border conflicts

feat: add multi-path icon support and improve navigation layout

October 24, 2025asce1062View on GitHub43024d0
Commit details
  • Switch around header and footer content

- Add support for multi-path icons in navigation (icon-female with 10 paths)
- Update Header layout to wrap from left edge in desktop mode
- Tighten navigation spacing to prevent RSS feed wrapping
- Add 8biticon link to main navigation with proper icon rendering

update: packages

October 24, 2025asce1062View on GitHube87e538
Commit details
@astrojs/check ^0.9.4 → ^0.9.5
@astrojs/mdx ^4.37 → ^4.3.8
@astrojs/rss ^4.0.12 → ^4.0.13
astro ^5.14.5 → ^5.15.1
astro-og-canvas ^0.7.0 → ^0.7.2
astro-themes ^0.2.6 → ^0.2.7
@eslint/js ^9.37.0 → ^9.38.0
@types/node ^24.6.2 → ^24.9.1
@typescript-eslint/eslint-plugin ^8.46.1 → ^8.46.2
@typescript-eslint/parser ^8.46.1 → ^8.46.2
astro-og-canvas ^0.7.0 → ^0.7.2
astro-themes ^0.2.6 → ^0.2.7
eslint ^9.37.0 → ^9.38.0
knip ^5.64.1 → ^5.66.2
lint-staged ^16.2.4 → ^16.2.6
netlify-cli ^23.9.1 → ^23.9.5

refactor: centralize share functionality with reusable notification component

October 24, 2025asce1062View on GitHubc49de9c
Commit details
  • Create ShareNotification.astro component for consistent notifications

- Add shareManager.ts utility for centralized clipboard operations
- Refactor avatar generator and blog share buttons to use shared utilities
- Move notification styles from avatar.css to global.css for site-wide availability
- Fix clipboard API user gesture chain by removing preventDefault
- Update service worker navigateFallbackDenylist regex patterns
- Ensure dark text color in notifications for better contrast
- Update icomoon fonts
- Update back to top icon

refactor: improve scroll UX and global navigation features

October 24, 2025asce1062View on GitHub5c5b259
Commit details
  • Add global scroll progress bar across all pages

- Implement floating back-to-top button with auto-hide behavior
- Appears after 250px scroll, auto-hides after 3s of inactivity
- Centered horizontally for mobile compatibility (48px) and desktop (56px)
- Smooth scroll animation with hover pause functionality
- Fix theme switcher initialization on first page load
- Add document.readyState check for proper event listener setup
- Manually update localStorage and data-theme attributes
- Remove duplicate event listeners to prevent conflicts
- Extract scroll progress from blog-specific to global utility
- Move from blogInteractions.ts to scrollProgress.ts
- Remove blog-only back-to-top button in favor of floating button
- Add conditional initialization for AvatarGenerator
- Prevent errors on pages without avatar canvas element
- Separate back-to-top logic into dedicated script module
- Create src/scripts/backToTop.ts
- Update knip.json for new file

feat: implement comprehensive site enhancements and optimizations

October 23, 2025asce1062View on GitHub7aeeb35
Commit details
## Blog Infrastructure
- Migrate blog posts to content collections with glob loader
- Create dynamic route /blog/[...slug].astro for blog post rendering
- Implement accurate reading time estimator based on word count (200 WPM)
- Add reading time display to blog cards and post layouts
- Update blog post paths from src/pages/blog to src/content/blog
- Update all MDX imports to use path aliases (@/)

Files: src/content/config.ts, src/pages/blog/[...slug].astro,
src/pages/blog/index.astro, src/pages/tags/[tag].astro,
src/layouts/BlogLayout.astro, src/lib/blog/utils.ts,
src/components/Post.astro

## SEO Enhancements
- Generate unique Open Graph images per blog post using astro-og-canvas
- Integrate content collections with OG image generation
- Add RSS feed auto-discovery meta tags for better feed detection

Files: src/pages/open-graph/[...route].ts,
src/components/seo/MetaTags.astro

## View Transitions & UX
- Implement Astro ClientRouter for SPA-like page navigation
- Add fade transitions between pages for smooth navigation
- Update all interactive components to support view transitions:
- Blog interactions (BlogLayout)
- Theme switcher (ThemeSwitcher)
- Search functionality (SearchField)
- GitHub contributions (GitHubContributions)
- Avatar generator (AvatarGenerator)
- Add astro:page-load event listeners for proper reinitialization
- Prevent duplicate search box initialization with state tracking

Files: src/layouts/Layout.astro, src/layouts/BlogLayout.astro,
src/components/ThemeSwitcher.astro, src/components/SearchField.astro,
src/components/GitHubContributions.astro,
src/components/AvatarGenerator.astro

## Dark Mode Improvements
- Add smooth CSS transitions for theme switching (0.3s bg, 0.2s colors)
- Implement smart exclusions to prevent animation conflicts
- Add prefers-reduced-motion support for accessibility
- Eliminate jarring theme switch flashes

Files: src/styles/global.css

## Footer Enhancements
- Add build timestamp to footer
- Display formatted build date alongside commit hash
- Capture build time at build-time via new Date().toISOString()

Files: src/data/navigation.ts, src/components/Footer.astro

## Implement Content Security Policy (CSP) headers:
- Add strict CSP directives to prevent XSS attacks
- Configure script-src with necessary inline permissions for Astro
- Restrict image sources to self, data URIs, HTTPS, and blobs
- Allow iframe embedding only from trusted GitHub contributions source
- Allow our service worker to fetch GitHub OpenGraph images from opengraph.githubassets.com
- Add Strict-Transport-Security with 2-year max-age and preload
- Enhance Permissions-Policy to restrict payment API access
- Add worker-src directive for service worker support
- Include connect-src for fetch/XHR restrictions

Files: netlify.toml

## Service Worker Enhancements
Add runtime caching strategy for GitHub OpenGraph images used in project cards:
- Cache GitHub OG images from opengraph.githubassets.com
- Use CacheFirst strategy for optimal performance
- Configure 30-day expiration with 50 image limit
- Enable offline support for project card images
- Reduce bandwidth usage by caching external images
- Eliminate network latency on repeat visits

Files: astro.config.mjs

## Technical Details
### CSP Directives
- default-src 'self' - Restrict all resources to same-origin by default
- script-src 'self' 'unsafe-inline' 'unsafe-eval' - Allow Astro hydration
- style-src 'self' 'unsafe-inline' - Allow inline styles for Astro
- img-src 'self' data: https: blob: - Support various image sources
- font-src 'self' data: - Allow local and data URI fonts
- connect-src 'self' https://jandee.vercel.app - Restrict API calls
- frame-src https://jandee.vercel.app - Only GitHub contributions iframe
- worker-src 'self' blob: - Support service workers and blob workers
- object-src 'none' - Block plugins like Flash
- base-uri 'self' - Prevent base tag hijacking
- form-action 'self' - Restrict form submissions
- frame-ancestors 'none' - Prevent clickjacking
- upgrade-insecure-requests - Force HTTPS for all resources

### Additional Security Headers
- X-Frame-Options: DENY (legacy clickjacking protection)
- X-XSS-Protection: 1; mode=block (legacy XSS protection)
- X-Content-Type-Options: nosniff (prevent MIME sniffing)
- Referrer-Policy: strict-origin-when-cross-origin (privacy)
- Permissions-Policy: Restrict camera, microphone, geolocation, payment
- Strict-Transport-Security: 2-year HSTS with preload eligibility

### Reading Time Algorithm
- Strip MDX frontmatter, imports, JSX, code blocks, and markdown syntax
- Count actual readable words
- Calculate based on 200 words per minute industry standard
- Provide fallback for empty content

### View Transitions Lifecycle
- Initialize on DOMContentLoaded for first page load
- Reinitialize on astro:page-load for client-side navigation
- Prevent duplicate initializations with state tracking
- Maintain functionality across page transitions

## Technical Notes

### Static Site CSP Limitations
While unsafe-inline weakens XSS protection, it's required for static Astro sites due to:
- Astro's component hydration using inline scripts
- astro-themes FOUC prevention scripts
- Service worker registration inline script
- View transitions client router inline code

## Performance Impact
- No hydration cost from view transitions (uses native API)
- Lazy search initialization with requestIdleCallback
- Optimized font loading with font-display: swap
- Build timestamp captured at build-time (zero runtime cost)

## Accessibility
- Automatic route announcements for screen readers
- prefers-reduced-motion support for theme transitions
- Proper ARIA labels maintained across transitions
- Skip-to-content links preserved
- GitHub OG Images: Instant loading after first visit (0ms latency)
- Offline Support: Project cards remain functional offline

feat: add commit hash display to footer and update environment configuration

October 23, 2025asce1062View on GitHub65e1037
Commit details
Footer Enhancements:
- Display current commit hash in footer with link to GitHub commit
- Format: "open source on commit [hash]. made with astro"
- Conditional rendering - only shows when COMMIT_REF is available
- Add Astro link to footer text

Site Metadata Updates:
- Add commit hash logic to siteMetadata in navigation.ts
- Add computed properties: commitHash, commitHashFull, commitUrl
- Add astroUrl to siteMetadata for centralized link management
- Create helper functions getFullCommitHash() and getShortCommitHash()
- All footer data now centralized in single source of truth

PWA Screenshot Updates:
- Add 5 mobile screenshots (narrow form factor, 1242x2688)
- Add 8 desktop screenshots (wide form factor, 1280x720)
- Add proper sizes attribute to all screenshots for validation
- Comply with PWA manifest screenshot limits

Add automatic GitHub OG image fallback for project cards
- Add getGitHubOgImage() helper function to src/lib/api/github.ts
- Parses GitHub repo URLs to extract owner and repo name
- Generates OpenGraph preview URL: opengraph.githubassets.com/1/{owner}/{repo}
- Returns null for invalid URLs with proper error handling

- Update Card component to use GitHub OG fallback images
- Import and utilize getGitHubOgImage() helper
- Maintain image priority: local image prop → imagePath lookup → GitHub OG
- Render GitHub OG images when no local images exist

Build & Deploy Configuration:
- Set COMMIT_REF=$(git rev-parse HEAD) in package.json predeploy
- Ensures commit hash available for GitHub Pages deployments
- Add COMMIT_REF to .env.example with setup instructions

feat: integrate GitHub API for dynamic project fetching and display

October 22, 2025asce1062View on GitHubd57d932
Commit details
  • Implement GitHub API integration to fetch repositories dynamically

- Create getFeaturedProjects() with pagination support (up to 1000 repos)
- Add support for sorting by stars, name, or recent activity
- Include language enrichment via parallel API calls
- Add optional fork filtering and project blacklist

- Add language icon mapping utility
- Map programming languages to icon classes
- Support case-insensitive matching with fallback logic

- Refactor projects page to use GitHub API
- Replace static JSON with live API data
- Display all languages per project (not just primary)
- Add fork indicators with icon-git
- Sort by most recently pushed repositories
- Support optional project preview images (jpg/jpeg/png/gif)

- Update reusable Card component
- Consolidate project card layout into single component
- Add optional image loading with automatic format detection
- Include star counter, fork badge, and language tags with icons
- Support responsive grid layout via Columns component

- Update homepage projects list to also use GitHub API
- Fetch top 4 starred projects dynamically

- Improve blog post layout and accessibility
- Redesign post layout

- Add environment configuration
- Create .env.example with placeholders for GitHub PAT and Netlify tokens

Technical Details:
- Use Promise.all for parallel API calls (language fetching)
- Implement pagination to fetch all repos (100 per page, max 10 pages)
- Support word-breaking for underscore-separated names (break-all)
- Prevent star counter squeezing with flex-shrink-0

updates: offline support and enhance SEO with Person schema

October 21, 2025asce1062View on GitHubcfdb1c5
Commit details
  • Update PWA offline functionality:

- Create /offline page with connection status detection
- Implement OfflineManager utility for last page tracking
- Add auto-redirect when connection is restored
- Configure service worker offline fallback in astro.config.mjs

* Enhance structured data and SEO:
- Add Person JSON-LD schema to OpenGraphTags component
- Include expertise areas, education, and social profiles
- Improve search engine understanding of personal identity
- Add google website verification file

* Update homepage and TypeScript configuration

feat: add copy link button

October 18, 2025asce1062View on GitHub6ec2966
Commit details
  • Add copy link button to blog social share buttons

- Create reusable CopyToClipboard utility in src/scripts/
- Add share button with icon-share icon and hover:-rotate-6 animation
- Copy format: "Title\nURL" for easy sharing
- Show notification on successful copy

fix: service worker intercepting URLs with query parameters and sending them to the 404 page

updates

October 17, 2025asce1062View on GitHubc61de47

fix(pwa): resolve service worker issues with XML files and development mode

October 17, 2025asce1062View on GitHub78644a4
Commit details
  • Added navigateFallbackDenylist to prevent XML files (RSS, sitemap) from

being redirected to 404 page
- Added 'xml' to globPatterns to ensure RSS and sitemap files are precached
- Excluded /fonts/icomoon/ directory from precaching to fix 404 errors
- Updated service worker registration to only run in production mode,
preventing 404 errors during development

fix(pwa): exclude icomoon font directory from service worker precaching

October 17, 2025asce1062View on GitHubd47513c
Commit details
Resolved service worker precaching error by adding globIgnores configuration
to exclude the /public/fonts/icomoon/ directory, which is used for version
control of font definitions and not actively served in the application.

- Added globIgnores to workbox configuration in astro.config.mjs
- Excluded patterns: /fonts/icomoon/ and **/fonts/icomoon

This fixes the browser console error:
"bad-precaching-response: {"url":"https://alexmbugua.me/fonts/icomoon","status":404}"

feat: SEO enhancements

October 17, 2025asce1062View on GitHubc4f330e
Commit details
Add extensive SEO optimizations across the site to improve search engine
visibility, social media previews, and accessibility:

Open Graph & Twitter Card Enhancements:
- Add og:image dimensions (1200x630 for articles, 400x400 for website)
- Add og:image:type for proper image display
- Add article:author and article:section metadata
- Add twitter:label1/data1 for reading time display
- Add twitter:label2/data2 for published date
- Implement reading time estimation utility (200 words/min)

JSON-LD Structured Data:
- Add BreadcrumbList schema for navigation hierarchy
- Add Organization schema alongside Person schema
- Add wordCount and timeRequired to BlogPosting schema
- Enhance schema with comprehensive social profiles

Security & Privacy:
- Add referrer policy (strict-origin-when-cross-origin)
- Add color-scheme meta tag for dark mode support

Accessibility Improvements:
- Add skip-to-content link for keyboard navigation
- Ensure proper focus management and ARIA support
- Verify lang="en" attribute on html element

Favicon Optimization:
- Generate favicon-16x16.png and favicon-32x32.png
- Add multiple favicon sizes for better browser support
- Maintain .ico fallback for older browsers

[feat] Implement Progressive Web App (PWA) and accessibility fixes

October 17, 2025asce1062View on GitHub8614467
Commit details
  • Add full PWA support with offline functionality and installability

- Fix accessibility tap target sizing (WCAG 2.1 compliance)
- Add adaptive theme colors for mobile browsers
- Generate optimized PWA icons with maskable variants
- Configure service worker with intelligent caching strategies

[chore] update README with Netlify deployment integration

October 17, 2025asce1062View on GitHube41e314
Commit details
### README.md
Added Table of Contents:
Added Netlify Deployment Documentation:
1. Setup: Link Repository to Netlify (One-time)
- Netlify UI method (step-by-step):
* Log in and import existing project
* Connect GitHub and authorize
* Configure build settings (base directory, build command, publish directory)
* Add environment variables (GUESTBOOK_ID, NETLIFY_ACCESS, NODE_VERSION)
* Configure custom domain (alexmbugua.me)
* Update site name (optional)
- Netlify CLI method:
* Install options (npx vs global)
* Authentication with netlify login
* Link repository (--name flag or interactive)
* Verify connection with netlify status
* Set up local .env file
* Why use npx vs global install (detailed comparison)
2. Automatic Git Deployment (Recommended)
- Push to main triggers automatic deployment
- Netlify runs build with Node 22
- Deploys dist directory
- Runs plugins for optimization
- Deploy notifications setup
3. Local Development with Netlify Features
- Test environment variables injection
- Test redirects and headers
- Test form submissions locally
- Runs on localhost:8888 (proxies Astro dev on 4321)
4. Manual CLI Deployment
- Production deployment: npm run deploy:netlify
- Preview/draft deployment: npm run deploy:netlify:preview
- Both npx and global CLI examples provided
5. Netlify Configuration
- Build settings (Node 22, dist output)
- Deploy contexts (production, preview, branch)
- Security headers (all 5 explained)
- Cache control (1 year immutable, 1 week images)
- Redirects (404 fallback)
- Netlify plugins (4 plugins with descriptions)
- Environment variables (required and auto-configured)
- Custom domain setup
6. Netlify Dev Notes
- When to use npm run dev vs npm run preview:netlify
- Testing Netlify features locally
7. Troubleshooting Netlify Builds
- Husky devDependency issue (expected behavior)
- Import resolution errors (dependencies vs devDependencies)
- Plugin failures (cache clearing)
- Environment variable issues (scope and injection)

feat: Add Netlify deployment integration

October 17, 2025asce1062View on GitHub30b1f49
Commit details
### netlify.toml
- Netlify configuration with build settings (Node 22, dist output)
- Local development configuration (Astro dev on port 4321, Netlify Dev on 8888)
- Production, preview, and branch deploy contexts
- Security headers:
- X-Frame-Options: DENY
- X-XSS-Protection, X-Content-Type-Options
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy for camera, microphone, geolocation
- Cache control headers:
- 1 year cache for immutable assets (JS, CSS, fonts)
- 1 week cache for images (PNG, JPG, WebP)
- 404 redirect handling
- 5 Netlify plugins configured:
* netlify-plugin-cache - Cache node_modules/.astro/dist for faster builds
* @netlify/plugin-sitemap - Automatic sitemap generation
* @netlify/plugin-lighthouse - Performance audits (90% thresholds)
* netlify-plugin-checklinks - Link validation (skips 404 pages)
* netlify-plugin-submit-sitemap - Auto-submit to Google/Bing/Yandex

### package.json
- Added Netlify deployment scripts:
* deploy:netlify - Production deployment
* deploy:netlify:preview - Preview/draft deployment
* preview:netlify - Local dev with Netlify features
- Installed dependencies:
* netlify-cli@^23.9.1 - Netlify command-line interface
* netlify-plugin-cache@^1.0.3 - Build caching plugin
* prettier-plugin-toml@^2.0.6 - TOML formatting support
- Updated lint-staged to include *.toml files for formatting

### .prettierrc
- Added prettier-plugin-toml to plugins array
- Added TOML file formatting override:
* 2 spaces indentation (no tabs)
* Consistent with JSON formatting rules

feat: add comprehensive image optimization configuration

October 16, 2025asce1062View on GitHubb2568df
Commit details
Implemented image optimization with responsive breakpoints,
modern formats (WebP/AVIF), and automatic srcset generation for optimal
performance across all devices.

### Image Service Configuration (astro.config.mjs)
- Configured Sharp image processor for high-quality optimization
- Set pixel limit to 268M (~16K image max) for safety
- Added WebP and AVIF format support for modern browsers
- Set default quality to 80 (optimal quality/size balance)

### Configuration Details
```js
image: {
service: {
entrypoint: "astro/assets/services/sharp",
config: { limitInputPixels: 268402689 }
},
formats: ["webp", "avif"], // Modern formats with fallbacks
quality: 80 // Balanced compression
}

### ImageView Component (src/components/ui/ImageView.astro)

- Added responsive srcset with Tailwind breakpoints [640, 768, 1024, 1280, 1536]
- Configured sizes attribute: (max-width: 640px) 100vw, (max-width: 1024px) 80vw, 1024px
- WebP format with quality 80
- Added TypeScript interface for widths and sizes props
- Maintains lazy loading and async decoding

### Post Component (src/components/Post.astro)

- Optimized thumbnail images with widths [144, 288, 432] (1x, 2x, 3x)
- Fixed size attribute: 144px for consistent thumbnails
- WebP format with quality 80
- Responsive images for Retina displays

### PreviewImage Component (src/components/PreviewImage.astro)

- Added responsive widths [640, 768, 1024, 1280]
- Configured sizes: (max-width: 768px) 100vw, (max-width: 1024px) 80vw, 1024px
- Higher quality (85) for hero/preview images
- WebP format for better compression

### Responsive Image Sizes Strategy

- Mobile (< 640px): 100vw (full width)
- Tablet (640-1024px): 80vw (80% width)
- Desktop (> 1024px): 1024px max
- Thumbnails: Fixed 144px with 2x/3x variants

- Standard images: 80 (optimal balance)
- Hero/preview images: 85 (higher quality)
- Thumbnails: 80 (sufficient for small sizes)

All images now support these props:
- widths: Array of image widths for srcset
- sizes: CSS sizes attribute for browser selection
- quality: Compression quality (0-100)
- format: Output format (webp, avif, jpg, png)
- loading: lazy (default) or eager
- decoding: async (default) or sync

feat: codebase refactoring, quality and tooling improvements

October 16, 2025asce1062View on GitHub73b8ca7
Commit details
### TypeScript Configuration (tsconfig.json)
- Removed deprecated baseUrl option
- Updated path aliases to use relative paths (./src/*)
- Enhanced strict mode settings:
- Added noUnusedLocals, noUnusedParameters
- Added noFallthroughCasesInSwitch
- Enabled allowSyntheticDefaultImports
- Enforced forceConsistentCasingInFileNames

### Markdown Configuration (markdown.config.ts)
- Fixed type incompatibilities with VS Code theme imports
- Added type assertions for rehype-pretty-code theme options
- Fixed remark-slug plugin type conflicts
- Resolved rehypeAccessibleEmojis type incompatibility

### Tailwind Configuration (tailwind.config.mjs)
- Integrated with design tokens from src/config/design-tokens.mjs
- Simplified configuration using centralized token system
- Added LightningCSS for faster CSS minification

### ESLint Configuration (eslint.config.mjs)
- Migrated to ESLint v9 flat config format
- Configured @typescript-eslint parser and plugin
- Added astro-eslint-parser for .astro files
- Integrated eslint-plugin-astro with recommended rules
- Set up TypeScript-aware linting with type checking

### Prettier Configuration
- Updated .prettierrc with comprehensive formatting rules
- Added prettier-plugin-astro for Astro file support
- Configured tab width, semi-colons, and quotes preferences
- Updated .prettierignore with proper exclusion patterns

### Configuration Organization
- Moved src/config.ts → src/config/site-config.ts
- Created centralized src/config/ directory for all configuration
- Updated all imports across 4+ files (Layout, MetaTags, OpenGraphTags, RSS)

### UI Components Organization
- Created src/components/ui/ directory for generic components
- Moved reusable components:
- Card.astro
- Columns.astro
- Expand.astro
- ImageView.astro

### Styles Organization by Tailwind Layers
- Reorganized CSS into logical structure:
- src/styles/global.css: Main entry with @layer base and components
- src/styles/components/: Component-specific styles
- avatar.css (Avatar generator)
- guestbook.css (Guestbook entries)
- search.css (Pagefind search)

### Husky Setup
- Installed husky@^9.1.7 and lint-staged@^16.2.4
- Added "prepare": "husky" script for automatic setup
- Created .husky/pre-commit hook
- Configured git hooks path: core.hooksPath = .husky

### Lint-staged Configuration
- JavaScript/TypeScript/Astro files: ESLint --fix + Prettier
- JSON/Markdown/CSS/YAML files: Prettier formatting
- Runs only on staged files (performance optimized)
- Automatically stages fixed files
- Integrates with existing .prettierignore rules

### Added Dependencies
- lightningcss@^1.30.2 (Fast CSS minifier)
- husky@^9.1.7 (Git hooks)
- lint-staged@^16.2.4 (Staged file linting)

### Updated knip.json
- Added lightningcss to ignoreDependencies (used by Vite)
- Added pagefind to ignoreDependencies (used by Astro integration)

## Scripts & Tooling
- Added scripts: format, lint, lint:fix
- Pre-commit hooks now enforce quality before commits

[refactor] Pages refactoring

October 16, 2025asce1062View on GitHub23b66b4
Commit details
### src/lib/api/guestbook.ts
- Guestbook API integration with Netlify Forms
- Type-safe GuestEntry interface for API responses
- Exported utilities:
- getGuestEntries(): Fetch submissions with error handling
- getInitials(): Generate avatar initials from name
- formatEntryDate(): Consistent date formatting
- Environment variable validation
- Centralized error handling

### src/components/GuestbookEntry.astro
- Dedicated component for rendering guestbook entries
- Consumes GuestEntry interface for type safety
- Handles conditional URL linking
- Separates rendering logic from page orchestration

### src/scripts/guestbookCounter.ts
- Nostalgic hit counter logic extraction
- Exported utilities:
- generateCounterValue(): Random 6-digit counter
- initGuestbookCounter(): Initialize counter display
- Follows established scripts pattern (themeManager, keyboardShortcuts)

### src/styles/guestbook.css
- 154 lines of extracted inline styles
- Theme-aware CSS with light/dark mode support
- Nostalgic web effects (blink, marquee, gradient animations)
- Entry layout and bubble styling
- Browser-cacheable separate from page logic

### src/lib/blog/utils.ts
- Shared blog post utilities to eliminate duplication
- Exported utilities:
- formatPostDate(): Consistent date formatting (long/short)
- getPostUrl(): URL generation from post ID
- sortPostsByDate(): Sort posts by pubDate (newest first)
- getAllTags(): Extract unique tags from posts
- Type-safe with CollectionEntry<"blog"> types

Created `knip.json` Configuration

October 16, 2025asce1062View on GitHub07643c3
Commit details
What We're Ignoring (and Why):

- pagefind dependency - Used by astro-pagefind integration (runs via Astro hooks)
- Demo files - public/fonts/icomoon/demo-files/ (IcoMoon preview files)
- Build scripts - scripts/
(deployment utilities like fix-rss.js, pre-deploy.js)
- Public API modules - These export functions intended for external use:
- src/scripts/blogInteractions.ts - Utilities like scrollToTop(), initBackToTop()
- src/scripts/keyboardShortcuts.ts - Public API: registerShortcut(), registerShortcuts()
- src/scripts/themeManager.ts - Theme utilities: getCurrentTheme(), isLightMode(), toggleTheme()
- src/data/navigation.ts - Configuration with public types: NavigationLink, SocialLink

Entry Points Configured:
- markdown.config.ts - Resolves rehype/remark plugin dependencies
- src/pages/**/*.{astro,mdx} - All page files
- src/layouts/**/*.astro - Layout components
- src/components/**/*.astro - Component files

Design Decisions

- All "unused" exports are intentionally part of the public API:
- Modules follow the "library pattern" - they export utilities even if not all are used internally
- Types are exported for consumer type safety
- This enables extensibility - users can import any utility they need
- Follows TypeScript best practices for module design

refactor: Complete refactoring of website codebase

October 16, 2025asce1062View on GitHube30dcd7
Commit details
### Blog Layout Refactoring (BlogLayout.astro)
- Created src/scripts/blogInteractions.ts
- Extracted back-to-top button logic
- Extracted scroll progress bar functionality
- Centralized all blog interaction handlers
- Created reusable blog components:
- src/components/blog/SocialShareButtons.astro
- Supports 6 platforms:
- WhatsApp, Facebook, X, Telegram, Pinterest, Email
- src/components/blog/TableOfContents.astro
- Recursive ToC renderer with 3-level nesting support
- src/components/blog/PostNavigation.astro
- Previous/next post navigation with CollectionEntry types

### Avatar Generator Decomposition (AvatarGenerator.astro)
- Created modular TypeScript modules:
- src/scripts/avatarStateManager.ts
- State management with URL synchronization
- Shareable avatar URLs
- src/scripts/avatarRenderer.ts
- Canvas rendering with image caching
- Thumbnail generation
- Created component directory src/components/avatar/:
- GenderSelector.astro - Male/Female radio buttons
- AvatarCanvas.astro - Canvas element with loading
- LayerTabs.astro - 6 customization tabs
- LayerGrid.astro - Layer item container
- AvatarActions.astro - Randomize, Download, Share
- Created src/styles/avatar.css
- Extracted all avatar-specific styles
- Responsive design patterns

### SEO Meta Tags Extraction (Layout.astro)
- Created src/components/seo/MetaTags.astro
- Core SEO meta tags
- Created src/components/seo/OpenGraphTags.astro
- OG and Twitter Card meta tags

### Inline Styles Migration
- Migrated style="cursor: pointer;" to Tailwind cursor-pointer class
- Zero inline styles remaining in codebase

### CSS Variables Consolidation
- Organized CSS variables in src/styles/global.css with categories:
- Color Palette (9 colors)
- Layout & Spacing
- Typography
- Added 15 section comment headers for better organization:
- Custom Font Faces
- CSS Custom Properties
- Global Selections & Body
- Base Typography
- Layout Elements
- Icon Font Styles
- Responsive Typography
- Component Typography
- Lists
- Interactive Elements
- Content Blocks
- Utility Components
- Code Blocks & Syntax Highlighting
- Prose & Content
- Table of Contents
- Global Utility Styles

### Global CSS Cleanup
- Removed 3 blocks of commented-out code
- Improved documentation and organization
- Better maintainability with clear sections

### Theme Management Extraction
- Created src/scripts/themeManager.ts
- Type-safe theme management (Theme = "light" | "dark")
- Exports: getCurrentTheme(), isLightMode(), toggleTheme()
- Exports: updateThemeIcon(), initThemeSwitcher(), handleThemeToggle()
- Eliminates global function pollution
- Refactored src/components/ThemeSwitcher.astro
- Clean module imports instead of inline scripts
- Proper event handling with DOM manipulation

### Keyboard Shortcuts Extraction
- Created src/scripts/keyboardShortcuts.ts
- Reusable shortcut handler with modifier key support
- Interface: ShortcutConfig with key/modifier mapping
- Exports: registerShortcut(), initSearchShortcut(), registerShortcuts()
- Cross-platform support (Ctrl/Cmd)
- Refactored src/pages/search.astro
- Reduced from inline implementation to 2-line import
- Ctrl+K / Cmd+K search focus functionality

### Navigation Configuration
- Created src/data/navigation.ts
- Single source of truth for all site navigation
- Type-safe interfaces: NavigationLink, SocialLink
- Exported configurations:
- mainNavigation: 8 footer links
- socialLinks: 2 social media links (GitHub, LinkedIn)
- contactLinks: 3 contact/resume links
- siteMetadata: site-wide metadata
- Refactored src/components/Header.astro
- Dynamic rendering from centralized config
- Easy to maintain and update links
- Refactored src/components/Footer.astro
- Dynamic .map() rendering from config
- Animation patterns preserved

### Type Safety Improvements
- Enhanced src/types.d.ts
- Added comprehensive JSDoc documentation
- Added readonly modifiers for immutability
- Created dedicated interfaces:
- ImageMetadata - separate image data interface
- PostProps - Post component props
- ImageViewProps - ImageView component props
- PreviewImageProps - PreviewImage component props
- Improved type reusability across components
- Better IntelliSense/autocomplete support

### Zod Validation Enhancement
- Enhanced src/content/config.ts
- Comprehensive blog collection schema validation:
- Title: 1-100 characters
- Description: 10-200 characters
- Image URL: must start with "/" (relative path)
- Image alt: 5-150 characters
- PubDate: cannot be in future
- Tags: 1-10 tags, auto-lowercase, must be unique
- Optional: draft, featured fields
- Helpful error messages for content validation
- Type inference for better developer experience (DX)

### Script Loading Optimization
- Optimized src/components/SearchField.astro
- Implemented requestIdleCallback for deferred initialization
- Pagefind UI only loads during browser idle time
- Fallback with setTimeout for older browsers
- Reduces main thread blocking, improves Time to Interactive (TTI)
- Added performance documentation to src/components/AvatarGenerator.astro
- Documents Astro's automatic async script bundling
- Notes on DOMContentLoaded initialization

### Image Optimization
- Enhanced 3 components with lazy loading and async decoding:
- src/components/ImageView.astro
- src/components/PreviewImage.astro
- src/components/Post.astro
- Added attributes to all Astro Image components:
- loading="lazy" - lazy load images below fold
- decoding="async" - non-blocking image decode
- Leverages Astro's built-in optimizations:
- Automatic WebP/AVIF generation
- Responsive image sizing
- Width/height inference (prevents CLS)

[feat] - add 8biticon pixel avatar icon generator

October 10, 2025asce1062View on GitHubb73be52

* updates

October 9, 2025asce1062View on GitHub2b4ac80
Commit details
  • autofocus search on page load

- guestbook entries layout

* updates

October 8, 2025asce1062View on GitHub7492259
Commit details
  • search quality of life

- update search tips
- add keyboard shortcuts to quickly access search
- add <kbd></kbd> element styles
- add matching functionality
- sort search by date descending
- update pagefind.d.ts
- add date type

* updates

October 7, 2025asce1062View on GitHub808edd9
Commit details
  • avoid data-pagefind-ignore wrapping

- dates, descriptions, tags, titles
- update search pagefind
- add Search Tips
- update footnotes layout

* updates

October 6, 2025asce1062View on GitHub8347330
Commit details
  • update blog

- add one about realignment
- update rss generator
- add search page link to BlogLayout
- add search page link to Footer
  • [feat] - add blog search

- add pagefind, astro-pagefind and pagefind default-ui
- add SearchField component
- add search page
- add search.css to override default-ui styles
- match website theme
- add a declaration file for module '@pagefind/default-ui'
- add pagefind.d.ts

*updates

October 3, 2025asce1062View on GitHubd2bd586
Commit details
  • move markdown configs to markdown.config.ts

- Footer component layout
- GitHubContributions layout
- Header link depth to inline-block
- BlogLayout add optional Table of Contents
- add remark-extract-toc package
- add remark-slug package
- add exposed tableOfContents to specific blogs
- Index housekeeping
- styles
- add table-of-contents
- adjust footer
- add Knip to audit unused packages
- remove unused packages from package.json
- re-generate package.lock.json

* update blog layout

October 1, 2025asce1062View on GitHubc43ad92
Commit details
  • add share blog section

- add navigate to top
- add blog navigation section
- add permalink frontmatter to posts
- add page scroll progress indicator

remove layout margins

September 30, 2025asce1062View on GitHub21334dd

- add github contributions heatmap

September 30, 2025asce1062View on GitHub9c8488d

- update base layout style

September 30, 2025asce1062View on GitHub0465254
Commit details
  • update code block layout

  • update code block theme

- add rosepine theme from vscode
  • update blog

- add tags to blog page
- use columns component for more flexibility
  • update tags

- use columns component for more flexibility

updates

September 29, 2025asce1062View on GitHubf1b6857
Commit details
  • add prettier

- format project
- update libraries
- columns component
- projects lists to 2 columns

updates

September 24, 2025asce1062View on GitHub8466134
Commit details
  • blog

- add one about finding good in everyday
- redirects
- rss feed

Updates

September 16, 2025asce1062View on GitHub3207c51

updates

September 16, 2025asce1062View on GitHube65cb1a
Commit details
  • add section tittles to timeline

- add skills components
- update resume with skills
- update projects data
- update header view resume link

update

September 15, 2025asce1062View on GitHub344d4f0
Commit details
  • add now page

- update icomoon pack
- add now page to Footer
- update guestbook

update resume

September 8, 2025asce1062View on GitHub2996029

update blog

September 5, 2025asce1062View on GitHubca99d7f
Commit details
  • add one about taking it one step at a time

updates:

August 29, 2025asce1062View on GitHub1cdbd5e
Commit details
  • update README

- update code block styling
- wrap content

* updates

August 28, 2025asce1062View on GitHub0cd13ca
Commit details
  • blog layout

- switch to row layout
- footer layout
- privacy policy layout
- add expand component
- update 404 content
- rss feed
- add copyright information

update:

August 22, 2025asce1062View on GitHub10ead6e
Commit details
  • file and folder persmissions

- add _headers
- add _redirects
- add robots.txt
fix:
- rss generator

update:

August 22, 2025asce1062View on GitHubfa14940
Commit details
  • rss feed generator

- scripts
- remove rss-fix

update

August 21, 2025asce1062View on GitHub9e8da5a
Commit details
  • rss feed header images

- add astro optimized images
- add rss-fix script
- adds xml:base attribute
- reorder namespaces

* update:

August 21, 2025asce1062View on GitHuba9dc911
Commit details
  • dependencies

- icomoon font with rss icon
  • add:

- rss feed for our blog

update: blog

August 21, 2025asce1062View on GitHubfa40abd
Commit details
  • add one about if it's not okay its not the end

fix: code block indentation

August 14, 2025asce1062View on GitHub6158685
Commit details
update: tip #7 usage examples headers

update:

August 13, 2025asce1062View on GitHub86fe131
Commit details
  • blog:

- Part 2:
- move saving/restore project tips to part 3
- update links to part 3
- add part 3: IcoMoon Mastery

fix:

August 11, 2025asce1062View on GitHubf2d171b
Commit details
  • links to part 2 icomoon workflow

- open-graph image generation font

update:

August 11, 2025asce1062View on GitHuba6db4d0
Commit details
  • blog

- part 1: add links to part 2
- add part 2: icomoon workflow
- styles/global.css
- ordered list styling within block quotes

update:

August 11, 2025asce1062View on GitHub0cb5367
Commit details
  • hosting your own site permalinks

* update:

August 11, 2025asce1062View on GitHub9981744
Commit details
  • update astro build config

- specify our assets output directory
- remove leading slash in build output
- update components/Header.astro
- update static link generation logic
- update layouts/Layout.astro
- fix static path links
- Use collections to query blog content
- add content config.ts
- use getCollection() instead of import.meta.glob() for our blog content
- update blog/index.astro
- use use getCollection()
- update pages/tags/[tag].astro
- use getCollection()
- update pages/tags/index.astro
- use getCollection()
- update styles/global.css
- ignore timeline and footnotes in ordered list
- update build and predeploy scripts
- build script: only run build
- predeploy script: remove no longer needed mv and run pre-deploy script commands
- update blogs
- fix verses

update: blog

August 6, 2025asce1062View on GitHub928763b
Commit details
  • add one about letting wounds heal

update:

August 1, 2025asce1062View on GitHub4bbb436
Commit details
  • Libraries:

- astro: 5.9.2 > 5.12.7
- astrojs/mdx: 4.3.0 > 4.3.2
- astrojs/sitemap: 3.4.1 > 3.4.2
- add check script: "npm run check"
- blog:
- add one about how to use IcoMoon to create and integrate high quality vector icon fonts
- ImageView component:
- add support for per image width adjustment
- Global css:
- fix code block responsiveness
- add ordered list styling
- add in-line code styling
- add blockquote styling
- add our IcoMoon generated files to version control
Fix astro check Warnings
- Remove Deprecated: Astro.glob()
- use import.meta.glob() to query source files
- add Frontmatter type

update: header SEO

July 28, 2025asce1062View on GitHub0f7e48f

update: alex.immer project tools

July 28, 2025asce1062View on GitHub6677ee6

* update:

July 28, 2025asce1062View on GitHub010cc5a
Commit details
  • deploy script

- use gh-pages for .jekyll and CNAME options
- blog
- add one about making room for love and peace

update blog

July 24, 2025asce1062View on GitHub716abbd
Commit details
  • add one about flowing with the current of life

- update code snipet in host your own site pt.2
- update case in resume

update: blog

July 17, 2025asce1062View on GitHuba8fc701
Commit details
  • add one about being present

Update README.md

July 10, 2025Alex Mbugua NgugiView on GitHub0ffbc5f

Update README.md

July 10, 2025Alex Mbugua NgugiView on GitHubaddc3a7

update: blog

July 9, 2025asce1062View on GitHub4a39259
Commit details
  • one about getting started, thank yourself later

update layout:

June 30, 2025asce1062View on GitHub2ae1f96
Commit details
  • Header search engine optimizations

- Footer
- fonts
- pre-deploy script
- open all external links in new tabs
update blog:
- add one about authenticity

update: blog

June 27, 2025asce1062View on GitHubef34922
Commit details
  • add one about consistency

update: guestbook and success pages
- improve folder structure
- update image locations

feat: add success page

June 26, 2025asce1062View on GitHub0fcd555
Commit details
  • add success.astro

- update guestbook to use custom redirect page
- add success.png

feat: add guestbook

June 26, 2025asce1062View on GitHubc14c863
Commit details
  • add guestbook.astro

- use netlify forms
- add images
- fetch guestbook entries
- style guestbook entries
- add privacy policy
  • switch to deploy using netlify

update: deploy scripts

June 26, 2025asce1062View on GitHube9110b7

update blog

June 24, 2025asce1062View on GitHub5184ec1
Commit details
  • add about theming with jekyll

- update typo in pt.1 host your own site
- update resume

update blog

June 24, 2025asce1062View on GitHubd121f85
Commit details
  • add one about perspective

update blog

June 23, 2025asce1062View on GitHub932412d
Commit details
  • add about the gift of hindsight

update ImageView component

add blog: how to host a README.md with github

June 23, 2025asce1062View on GitHub1dee0e9
Commit details
  • add ImageView component

update blog resume: include link to andela profile
- update Timeline component

add link to andela's profile partner view

June 23, 2025asce1062View on GitHub83ee618
Commit details
  • update timeline component

- add optional more information
- update variable names

fix typo

June 22, 2025asce1062View on GitHubfa9928f

update tag descriptions

June 21, 2025asce1062View on GitHube2bb220
Commit details
update blog
- update time image
- add about trusting the process
update timeline components
- add typing

update website README

June 20, 2025asce1062View on GitHube7e12be
Commit details
update blog page name
update footer project link
update blog and tag content grouping
open music site on a different tab

add resume blog page

June 19, 2025asce1062View on GitHub1a1d35a
Commit details
add timeline component
add motivation blog

feat: add blog

June 19, 2025asce1062View on GitHub3698459
Commit details
  • add blog page

- add BlogLayout component
- add post component
- add PreviewImage component
- add typing
- add first post
feat: add tags
- add tags page
- add indexed tags page
feat: add OpenGraph images for all pages
- add [..route]
- update all layouts
- update all pages
  • fixes and updates

- update icon pack style and font
- update pre-deploy script
- update header
- fix missing icons in project.json
- fix icon names in project.json

feat: add projects

June 17, 2025asce1062View on GitHubba8a4f6
Commit details
  • add card component

- add column component
- update index page
- add dev icons to fonts
fix: nested files accessing root assets
- update pre-deploy script

- update link share preview

June 17, 2025asce1062View on GitHubab7074c

feat: add showcase section

June 16, 2025asce1062View on GitHub6de1473

update summary language

June 16, 2025asce1062View on GitHub860120a
Commit details
update Resume

icomoon: only export fonts we are using

June 16, 2025asce1062View on GitHub69c0d28
Commit details
  • a huge font repository skews the project language

update README

June 16, 2025asce1062View on GitHub754f68c

update Header

June 16, 2025asce1062View on GitHub27a38b4
Commit details
update fonts with bootstrap icons v1.13.1 set

update Resume

June 15, 2025asce1062View on GitHubc295624
Commit details
add link to personal music site

update resume

June 15, 2025asce1062View on GitHub81d5221

FIx: site name

June 14, 2025asce1062View on GitHubaed48d9
Commit details
  • update predeploy script

- create and populate CNAME with our address

update resume link on README

June 14, 2025asce1062View on GitHub2910271

update resume.pdf

June 14, 2025asce1062View on GitHub6dc3ee5

update resume

June 14, 2025asce1062View on GitHub96758b6

Feat: artifacts and deployment to separate branch

June 14, 2025asce1062View on GitHubc74e8a8
Commit details
  • use gh-pages library to automate push and build

- add .nojekyll to prevent skipping _asset folders
- rename _astro to astro
  • remove _config.yml from root directory

  • update pre-deploy and deploy commands

  • rename post-build scripts

use gh-pages to push and build

June 14, 2025asce1062View on GitHub74c10ff

Create static.yml

June 14, 2025Alex Mbugua NgugiView on GitHub78d418f

remove static files from main branch

June 14, 2025asce1062View on GitHubcfda755

- restart workflows

June 13, 2025asce1062View on GitHub84990d7

remove static.yml file

June 13, 2025asce1062View on GitHuba08be49

Create CNAME

June 13, 2025Alex Mbugua NgugiView on GitHub0a649da

Delete CNAME

June 13, 2025Alex Mbugua NgugiView on GitHub3657a59

Delete CNAME

June 13, 2025Alex Mbugua NgugiView on GitHub9312d2b

Create CNAME

June 13, 2025Alex Mbugua NgugiView on GitHubcad83d3

Create static.yml

June 13, 2025Alex Mbugua NgugiView on GitHube5f9ab9

remove static.yml file

June 13, 2025asce1062View on GitHubdf7a6ed

remove demo files

June 13, 2025asce1062View on GitHub5ff7e88

Fix: blank page on render

June 13, 2025asce1062View on GitHubf3a53af
Commit details
  • use root directory instead

try the root directory

June 13, 2025asce1062View on GitHub8257b0e

Create static.yml

June 13, 2025Alex Mbugua NgugiView on GitHub77f9bff

revert to working point

June 13, 2025asce1062View on GitHubc8f1bf3

remove static file

June 13, 2025asce1062View on GitHubbd02959

Create CNAME

June 13, 2025Alex Mbugua NgugiView on GitHub72a0f71

move cname and config to static file directory

June 13, 2025asce1062View on GitHub6314b01

Create CNAME

June 13, 2025Alex Mbugua NgugiView on GitHub8818066

Delete CNAME

June 13, 2025Alex Mbugua NgugiView on GitHubece76f1

Delete CNAME

June 13, 2025Alex Mbugua NgugiView on GitHubac04e8c

Feat: add post build script

June 13, 2025asce1062View on GitHubf3319b9
Commit details
  • remove leading forward slash to linked generated stylesheets

- move asset links one level up on generated stylesheets
- Copy static files /dist to ../../docs/
Fix: leading slash breaking static path
Fix: generated css static file paths

Create CNAME

June 12, 2025Alex Mbugua NgugiView on GitHub20c105e

feat: rewrite in astro.

June 12, 2025asce1062View on GitHubb24c67c

feat: create home page

June 12, 2025asce1062View on GitHubb929386
Commit details
  • Use Astro and tailwind for styling

- use icon fonts (iconmoon)
- add light theme

- use bootstrap icons

June 11, 2025asce1062View on GitHub898243b
Commit details
  • align text and icons

add title

June 11, 2025asce1062View on GitHub85777d4

- view only on github

June 11, 2025asce1062View on GitHub81bb6b3

- download or view resume

June 11, 2025asce1062View on GitHube2fd66b

- improve project structure

June 11, 2025asce1062View on GitHubfadca0a

- Fix: word wrap on smaller screens

June 11, 2025asce1062View on GitHubddceb12
Commit details
  • make our table and elements more responsive

Bump ossf/scorecard-action from 2.4.0 to 2.4.2

June 10, 2025dependabot[bot]View on GitHub0b3da26
Commit details
Bumps ossf/scorecard-action from 2.4.0 to 2.4.2.
  • Release notes

  • Changelog

  • Commits


---
updated-dependencies:
  • dependency-name: ossf/scorecard-action

dependency-version: 2.4.2
dependency-type: direct:production
update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

switch README and website to vertical list

June 10, 2025asce1062View on GitHub9c7022a

Merge remote-tracking branch 'origin/main'

June 10, 2025asce1062View on GitHuba5ee431
Commit details
  • origin/main:

Create static.yml

- add resume download

June 10, 2025asce1062View on GitHubaf8d4a1

Create static.yml

June 10, 2025Alex Mbugua NgugiView on GitHubf06fc4d

** maintain separate github and website profiles

June 10, 2025asce1062View on GitHub35d2d8a
Commit details
  • README

- remove reference to github
- re-order contact information
* website
- initial commit
- add index.html
- add styles and fonts

- remove site header

June 10, 2025asce1062View on GitHub029819b

- remove static file workflows

June 10, 2025asce1062View on GitHub3c362c9
Commit details
  • add scorecard

  • add dependabot

  • update website link

Create static.yml

June 10, 2025Alex Mbugua NgugiView on GitHubfa07540

- start adding details

June 10, 2025asce1062View on GitHubbefa8a1
Commit details
  • add Resume

  • add CNAME

Initial commit

June 10, 2025Alex Mbugua NgugiView on GitHube687e80

All 491 commits loaded

All 491 commits fetched at build time. No runtime API calls.

Colophon · Site diagnostics


                                              a47d1e3 • • 10/26/2026

Alex's Workstation

stellar console

last seenback the same day

visits1

signalrestoring context

incoming transmission860×580

navidrome.exe|loading_playlist

>waiting_signal.mp3
[ carrier search... ]
Your browser does not support the audio element.

playlists

loading
Loading public playlists...

Search

Indexed: Search by content, dates, description, excerpts, tags or titles

Exact phrases: Use quotes like "I'm happy you're here"

Fuzzy matching: Partial matches like config will find "configuration"

Multiple terms: Search for using icomoon to find posts containing both words

Shortcuts: Use Ctrl+K or Cmd+K for quick access