This is a changelog page1.
$ git log --oneline _
My Tinkering Log
177 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:
2026
37d5fb6 fix: update stale asset paths from public/ restructure
asce1062
View on GitHub
Commit details
- Replace ogImage="social-preview.png" with "images/social-preview.png"
to public/images/social-preview.png
- Update privacy.astro CLI snippet: gpg --import path updated to canonical
/downloads/public.pgp
0a69985 refactor: migrate to Shiki built-in Rose Pine themes and reorganize email templates
asce1062
View on GitHub
Commit details
- Replace custom VS Code JSON theme exports with Shiki string
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).
505c096 refactor(public): reorganize static assets into icons/, images/, downloads/
asce1062
View on GitHub
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.
- 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.
8d9e488 misc(chore): visual improvements
asce1062
View on GitHub
e1c8a62 refactor(avatar): make randomize preview-only when a saved avatar exists
asce1062
View on GitHub
Commit details
Randomize previously used
logically inverted from the intended behavior. Changed to
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
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().
persist: avatarStore.isRemembered(),logically inverted from the intended behavior. Changed to
persist: !avatarStore.isRemembered() across the mini widget andAvatarStateManager 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().
f773526 fix: guard e.key before toLowerCase in keydown handlers
asce1062
View on GitHub
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
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
133defc fix(api): replace single encoded state param with separate gender+avatar params
asce1062
View on GitHub
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.
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.
cf1f930 fix(pwa): exclude API routes from SW navigation fallback and CDN caching
asce1062
View on GitHub
Commit details
astro.config.mjs:
- Add
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)
- Add
/^\/api\// to navigateFallbackDenylist so API navigationrequests 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)
fc52d05 fix(avatar): respect saved avatar when switching gender
asce1062
View on GitHub
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.
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.
7a3bcf3 fix(contributions): clip iframe white body bleed via overflow-y-hidden
asce1062
View on GitHub
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.
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.
dd33402 feat: avatar system, match-device theme, feedback manager hardening, and privacy updates
asce1062
View on GitHub
Commit details
Avatar system
- Add AvatarMiniWidget component and avatarMiniWidget.ts with render race
guard
AbortController-based lifecycle cleanup
- Add
localStorage persistence via getPref/setPref, and cross-tab clear support
- Add
layer loading, zIndex ordering), state serialization/deserialization with
strict /^\d+$/ + Number.isFinite + Number.isInteger validation
- Add
canvases from data-avatar-state attributes
- Add
AbortController lifecycle, hasDeliberateChoice guard, submitFallbackApplied
double-submit guard, and avatar state injection into form submission
- Add
endpoint with MAX_STATE_LENGTH=256 guard, duplicate param rejection,
and HTTP 400/500 semantics
- Add
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
prefers-color-scheme with AbortController cleanup, URL ?theme= override
precedence, and astro:after-swap re-stamp
- Harden
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
- Add
completion timers independently from auto-hide timers
- Add
cancellation; add primaryIcon === checkIcon same-element guard
- Guard
- Extract FADE_OUT_DURATION = 300 constant
- Replace clone-and-replace listener pattern with data-feedback-bound
dataset guard in initShareNotification
- Add
Preferences and privacy
- Add
- Update
(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
- Add AvatarMiniWidget component and avatarMiniWidget.ts with render race
guard
renderSeq, duplicate binding guard data-widget-bound, andAbortController-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 concurrentlayer loading, zIndex ordering), state serialization/deserialization with
strict /^\d+$/ + Number.isFinite + Number.isInteger validation
- Add
entryAvatarRenderer.ts: renders pixel art stamps on guestbook entrycanvases from data-avatar-state attributes
- Add
guestbookAvatarWidget.ts: guestbook avatar opt-in flow withAbortController 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 compositingendpoint with MAX_STATE_LENGTH=256 guard, duplicate param rejection,
and HTTP 400/500 semantics
- Add
src/lib/email/helpers/avatarImage.ts: sharp-based PNG compositorreturning 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 OSprefers-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-outcompletion timers independently from auto-hide timers
- Add
iconRestoreTimers WeakMap for per-element flashCheckIcon restore timercancellation; 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 mutationsPreferences and privacy
- Add
match-device-theme and avatar-state keys to PREF_KEYS- Update
PrivacyPolicy.astro, privacy.astro, andpublic/.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
d934510 refactor(toc): rewrite scroll behavior, sticky detection, dead zone, and rAF batching
asce1062
View on GitHub
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)
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)
c654acb feat: preferences system, CSS utilities split, privacy page redesign
asce1062
View on GitHub
Commit details
Preferences & storage
- add
- centralized localStorage helpers (getPref/setPref/removePref) and PREF_KEYS constants
- update
- 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
- 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
- isMac computed once at module level (not per keydown)
-
-
CSS utilities
- split
- 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
- 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 + AbortControllerCSS 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
3568fa1 feat: sidebar navigation, ASCII widget system, new pages, and site-wide polish
asce1062
View on GitHub
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
- Nav links with active state highlighting and icon tilt on hover
- Avatar footer with controls
- ASCII easter egg section: random art on load, dice randomize, brand name
badge updates to match the art's text value on dice click
- sidebarNav.ts: keyboard nav, AbortController cleanup across soft-navs
ASCII widget system
-
font label, and art <pre>, all namespaced under a widgetId prop
-
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
-
render (art + font label + data-ascii-current), dice wiring, reveal setup;
onRender callback for side effects; used by sidebar, about, and 404
-
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
- 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 stylesheetsNew 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
2a15279 feat: email notifications, admin moderation hub, security hardening, dead code cleanup
asce1062
View on GitHub
Commit details
A broad feature + housekeeping pass spanning the guestbook subsystem,
admin infrastructure, email delivery, and project tooling.
EMAIL NOTIFICATIONS
Integrate Resend for transactional email on new guestbook submissions:
Owner notification
- 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
- 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
- Handles both sends in parallel.
- Swallows per-send errors to keep them non-blocking.
- Logs outcomes without leaking PII to the console.
- centralizes From/Reply-To addresses, subject-line
templates, and the Resend API key reference in one place.
ADMIN SECTION
- 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.
- 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
Approve & Clear - set status → visible AND wipe flag reasons/score
Hide - set status → hidden
monospace table:
- entry deep-link
- action badge
- status transition
- 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
- strips CR/LF copy-paste artifact guard
- trims whitespace
- caps at 256 chars (arbitrary sanity limit to prevent DoS attempts with huge input)
- 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.
- reads ADMIN_TOKEN from env, guards against unset env var,
delegates to safeEqual via sanitizeToken on both sides.
- consistent cookie options on both set and delete
(path:/admin, httpOnly, secure:prod-only, sameSite:strict, maxAge:7d)
to prevent orphaned cookies.
- validates Origin header.
- Falls back to Referer for older browsers
- Explicitly rejects the string "null" sent by sandboxed iframes.
- 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
Centralize cross-cutting admin concerns so individual pages stay lean:
- pathname === "/admin" || pathname.startsWith("/admin/")
- the trailing-slash prefix prevents false matches on /administer etc.
- every admin POST is checked via
page logic runs.
- returns 403 Forbidden on failure.
- 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.
- /admin/* sub-pages redirect to /admin if auth fails.
- The /admin hub itself handles its own login form.
- applied to every return path, including the early
CSRF 403 and auth redirects so no response ever escapes without the
full security header set:
DATABASE & GUESTBOOK API
Add
- entryId
- action
- fromStatus
- toStatus
- reasonsBefore
- scoreBefore
- actor
- at
- no arg returns the full log for the audit tab.
- with entryId returns per-entry history for future detail views.
- gains a clearFlags option that zeroes out
moderationReason and moderationScore when approving false positives.
- 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
Extend
→ "Jan 1, 2024") to sit between
(2024-01-01).
Remove
from
options. All consumers updated to call
DEAD CODE CLEANUP
Delete
- re-export barrel with zero consumers.
Remove
- no call sites in the codebase.
- URL construction handled inline or via Astro.url.
Remove
- changelog.astro never imported it.
- entries are grouped directly in the template.
Remove
- used only internally to derive hexLight/hexDark, never consumed outside the module.
PGP / SECURITY FILES
Rotate public PGP key
-
-
-
-
Add WKD (Web Key Directory) support
-
- so mail clients can auto-discover the key from the domain
when users click the email address in the guestbook entry.
Update
TOOLING
- add
- add resend and validator.js
- update dependencies.
admin infrastructure, email delivery, and project tooling.
EMAIL NOTIFICATIONS
src/lib/email/src/lib/api/guestbook-notify.tssrc/config/email-config.tsIntegrate 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.astrosrc/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 flagsApprove & Clear - set status → visible AND wipe flag reasons/score
for false positivesHide - set status → hidden
Audit Trail tab renders the full GuestbookModerationLog as a compactmonospace 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.tssanitizeToken()- 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.tsCentralize 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 anypage 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.tssrc/lib/api/guestbook.tsAdd
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.tsExtend
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 localeoptions. All consumers updated to call
formatDate(..., "medium") orformatDate(...) directly.DEAD CODE CLEANUP
src/config/src/lib/api/github.tsDelete
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.sigAdd 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.jsoneslint.config.mjspackage.jsonknip.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.
47beb5e feat: add guestbook entry action menu and improve cross-browser background pattern support
asce1062
View on GitHub
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
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
3058e53 feat: refine guestbook customizer UI and entry card layout
asce1062
View on GitHub
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
- 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
92b3a02 feat: add guestbook entry theme customizer with Hero Pattern backgrounds
asce1062
View on GitHub
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:
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
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
32e011c feat: migrate guestbook from Netlify Forms to Astro DB + Turso
asce1062
View on GitHub
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
- 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
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
2b412de refactor: replace remark-reading-time with Astro recipe, add external link icons
asce1062
View on GitHub
Commit details
- Replace remark-reading-time + bridge plugin with a custom remark plugin
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.
d489a1c feat: add notes collection, unified content architecture, privacy/well-known infrastructure
asce1062
View on GitHub
Commit details
Notes & Content Architecture
- Add Notes collection with schema, utilities, and listing page (/notes)
- Merge BlogLayout + NoteLayout into unified ContentLayout with
- 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 (
- 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
- 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.
- 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.
50307e1 fix: default ogImage to social-preview.png in Layout
asce1062
View on GitHub
Commit details
Use SEO.ogImage as fallback when ogImage prop is not provided,
preventing blank social preview images on pages without explicit ogImage.
preventing blank social preview images on pages without explicit ogImage.
b1cb699 feat: add changelog page with GitHub commit history
asce1062
View on GitHub
Commit details
- Add /changelog page displaying all commits fetched at build time
- 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
ca5d9e1 feat: add palette page with design tokens and click-to-copy
asce1062
View on GitHub
Commit details
- Add /palette page with tabbed Swatches/Ingredients views
- 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
2ec25be feat: add colophon/meta pages, refactor config to grouped exports
asce1062
View on GitHub
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
- /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
2025
All 177 commits loaded