Changelog
I love that my site is a constant work in progress thanks to AI. I can add new features, content types, tweak things to improve them, easily fix bugs, and keep my site up to date with modern trends. Nothing holds me back from iterating as much as I want or need. A website shouldn’t be set-it-and-forget-it, but it was before AI because changes were so time consuming or expensive.
This page is the record of this ongoing process: the ideas that worked, the ones I walked back, the small fixes nobody else would ever notice. I like that my site is never really finished 🎉
Events now speak Google’s language
Added Event JSON-LD to the homepage’s Upcoming Events loop. front-page.php builds a @graph of Event objects (name, url, startDate, endDate, location, attendance mode, performer, description) straight from the same ACF fields already driving the visible cards. Google can now potentially build rich result cards for talks I’m speaking at, without a second data source to maintain. Virtual events are auto-detected from the location text and marked VirtualLocation instead of Place.
There's no single-event template. ms_event posts don't get their own public pages, so the schema had to live inline in the homepage loop, the only place events are ever rendered.
Events stop asking you to state the obvious
event_end_date used to require an explicit value even for a single-day event. front-page.php now defaults the JSON-LD endDate to startDate when the field’s left blank. The ACF field stays around only for genuine multi-day events.
Noticed after filling out the event admin screen one too many times myself. The fix is a one-line fallback, but it's the kind of papercut you only feel once you're the one using your own forms.
Built this changelog, the page you’re reading
New ms_changelog post type plus a feed template styled after the About page’s timeline: version badges, a live stats strip computed at render time (entry count, current version, how long this has been going), and filterable categories. Entries here aren’t hand-written after the fact. After any session that ships something worth mentioning, a draft entry gets written straight into this post type for review before it goes live.
Kept the same everything-editable-from-wp-admin pattern the rest of the site uses for Events, Media, and Timeline, rather than hardcoding entries into the theme. Future entries are WordPress posts, not code.
Event titles now look like links, and can point anywhere
Event cards get a custom CTA label field. Set it, and the button links straight to that event’s own URL with your own text (e.g. “Register now”) instead of always falling back to “book a meeting with me.” Titles also got restyled in the accent color with a small ↗ marker so they read as clickable at a glance, instead of blending into the surrounding text.
Kept the "book a meeting" fallback rather than removing it. Some events on the list are conferences where I'm speaking and a meeting link makes sense, others need an actual registration link. The field just lets each event pick.
Fixed embeds and a nav gap on mobile
Standalone embed blocks (YouTube, Vimeo, etc.) were overflowing the viewport edge on phones. Fixed by wrapping them in an aspect-ratio:16/9 container with the iframe positioned to fill it, rather than the older padding-bottom percentage trick. Separately, a blank gap opened up under the nav on small screens: the page reserves 60px of top padding to clear the fixed nav bar, but the nav itself switches to position:relative at 640px, so that padding became pure dead space. Zeroed out below that breakpoint.
Launched a newsletter called The Diff
The Diff, a biweekly read on AI and WordPress, publishes as regular posts in a dedicated category rather than a custom post type. Every issue is automatically indexed, shows up in RSS, and appears in the blog archive and homepage Latest Posts with zero extra plumbing. A the_content filter auto-injects the newsletter masthead and subscribe button onto any post in that category, so there’s no manual step needed per issue.
The first version of the subscribe button linked to home_url('/#newsletter'), which navigated away from the post entirely before scrolling. Fixed to a same-page #newsletter anchor, since the signup form already lives in the footer of every page.
Brought back multi-column layouts and text highlights in posts
Dequeuing WordPress’s block-library CSS for performance had a side effect nobody noticed until writing a post that needed it. Column blocks stopped laying out side by side, and the highlight/mark tool’s colored background disappeared. Both got a small targeted CSS hook instead of re-enabling the block library wholesale. Restoring 30KB-plus of unrelated CSS for two rules felt like the wrong trade.
The mark highlight needed !important. The block editor writes its own inline background-color style directly on the element, which otherwise wins over anything in the stylesheet.
Added a newsletter signup, the boring way on purpose
The Kit signup form in the footer is a static HTML embed, not the async script Kit normally recommends. The async version creates a timing race: the script injects the form after the page has already rendered, so any CSS overrides applied before the form exists get silently ignored. Three different MutationObserver-based workarounds all failed or hurt performance before landing on the simplest option: paste the HTML directly into the template, so the form is already in the DOM before any script runs.
Reordered the homepage so the writing comes first
Latest Posts now sits directly under the hero instead of further down the page, with a “see all posts” button. The hero itself shrank to a square 260px photo with less padding, since its job is just to say who I am before you scroll to what I actually made.
Shrinking the hero took a few tries. Removing the min-height and padding wasn't enough on its own. A leftover max-width:510px on the bio text kept forcing extra line wraps that made the section look tall no matter what else changed.
Fonts stopped blocking the page from rendering
Google Fonts now load with the classic async trick: <link rel=preload as=style>, media=print swapped to all via onload, plus a <noscript> fallback for anyone without JS. That one change removes roughly 1,200ms of render-blocking time on every page, applied globally.
The onload attribute uses single quotes (onload="this.media='all'"), which is a landmine inside a PHP echo string: it throws a fatal error. The fix is to drop out of PHP mode entirely for that one tag rather than trying to escape the quotes.
Killed the light-mode flash on every page load
Dark mode preference was being applied by a deferred JS file, which runs after the page has already painted, so anyone in dark mode saw a flash of light mode on every navigation. Fixed by moving the localStorage check into a tiny inline script in <head>, so the dark attribute is set before the browser paints anything. About 150 bytes, no extra network request.
The media card spacing bug wasn’t a CSS problem
Spent ten separate CSS attempts trying to add breathing room above a card’s footer line before finding the actual cause. The description text uses -webkit-line-clamp to truncate at 3 lines, and that clamping mode collapses the element to zero height inside a flex container once flex-grow is removed, so any margin-bottom on it had nothing to push against. The fix is a plain empty <div style=”flex:1;min-height:20px”> sitting in the template between the description and the footer: a real DOM element with real height, which no CSS trick could substitute for.
Pages start loading before you click them
Added Speculation Rules (Chrome) and instant.page (everywhere else) so top-level pages and the latest post start prerendering or prefetching the moment you hover a link. By the time you click, most of the page is already there. Also dequeued wp-embed.min.js, a script WordPress loads on every page by default even though this theme never uses WordPress’s own embed blocks.
View Transitions, the page-fade feature from a couple of versions back, got removed here. It kept colliding with the theme's own scroll-in fade animations for a worse effect than either alone. instant.page and Speculation Rules deliver the snappy feeling without that conflict.
Raised the readability floor across the whole site
Body text sitewide now has a 15px, font-weight 500 floor. Anything smaller or lighter than that was hard to read on some screens. Also added a real hamburger menu for mobile nav, since the menu previously just wrapped awkwardly, and fixed a dark-mode contrast issue on the primary button (near-black text on the accent orange, roughly 8:1 contrast).
Stopped dark mode from double-animating on toggle
Installed the browser’s View Transitions API for smoother page navigations, but it collided with the theme’s own CSS color transitions on body, .site-nav and .site-footer. Both tried to fade the same elements at once, causing visible jitter in dark mode, worst on mobile. Fixed with a :root:has(::view-transition) selector that suppresses the theme’s own transitions only while a view transition is actually running.
This one didn't survive long. See a couple of entries down.
Made the media grid load faster and read better
The first appearance card in the grid now gets fetchpriority=”high” and loading=”eager” so the browser fetches it immediately instead of waiting in the lazy-load queue. It’s usually the largest visible image on the page. Card titles switched from <h3> to <h2> to fix a heading-order gap: the page’s <h1> had no <h2> underneath it. The active filter tab’s background also moved off the site’s decorative orange (–accent) onto the AA-compliant text color (–accent-text), since the decorative shade fails contrast for anything you’re supposed to read.
The modal player's aria-labelledby stopped announcing itself correctly to screen readers while hidden. Turns out aria-labelledby is ignored entirely on elements inside a hidden subtree. Switched to aria-label, updated by JS when the modal opens.
Started as a design brief and static HTML, not a WordPress site
This site started as a design brief (retro internet aesthetic, high contrast black, one bold accent color, Fraunces + IBM Plex Mono) which was turned into a static HTML/CSS/JS prototype. Built entirely through a conversation with Claude. Another vibe-coding tool tried first and lost consistency across pages: the footer looked different every time, fonts drifted, even the placeholder photo kept shifting, because each page was generated somewhat independently of the others. Claude held it together by keeping every page on one shared stylesheet and script file, so nothing could drift between them. Only once that static version was solid did it get converted into the actual theme, custom post types, ACF fields, and all, that everything since has been built on.
The first working version of the theme had almost nothing editable from wp-admin - the timeline, media cards, and bio text were all hardcoded straight into PHP templates. Rather than hand-fix each one, the content got regenerated as WordPress XML import files instead: three WXR files, one each for timeline items, media appearances, and posts, ready to load via Tools -> Import. Went from an empty install to a populated site in minutes, and that same import pattern is still how bulk content gets added today.