Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const { title } = Astro.props;
<nav aria-label="Main" class="main-nav">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/projects">Projects</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
Expand Down
34 changes: 18 additions & 16 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,33 @@ import BaseLayout from '../layouts/BaseLayout.astro';
A few years ago, blind users struggled just to figure out where a
control lived on a page - top-left, buried in a sidebar, or three
tab-stops past a broken landmark. JAWS 2026&rsquo;s
<a href="https://www.youtube.com/watch?v=0Di913vXLOw" target="_blank" rel="noopener noreferrer">Page Explorer<span class="visually-hidden"> (opens in a new tab)</span></a>,
<a href="/resources">Page Explorer</a>,
paired with AI, can now describe an entire page&rsquo;s layout in detail,
in seconds. That&rsquo;s not incremental improvement. That is the exact
kind of threshold Clarke was talking about - things that were once impossible are suddenly ordinary.
</p>

<p>
It works because good code makes it possible. Page Explorer can only
describe a page well if it was actually built with real
structure underneath it - not div soup. That is the same reason
a single misplaced comma can break an entire application, and the
same reason a single design choice, like where a control sits on a
page, can make or break a user&rsquo;s ability to use it. Building
technology is not just about presentation. It is about experience.
When I started writing software in the early 2000s, IDEs had no real
concept of a screen reader. JAWS and NVDA weren&rsquo;t supported so
much as tolerated &mdash; you learned to work around your tools, not
with them. Twenty years later, VSCode ships an accessible view built
specifically for output like mine, no workaround required. That gap
didn&rsquo;t close on its own. It closed because a few engineers
followed simple rules &mdash; real structure, real landmarks, real
semantics &mdash; instead of bolting accessibility on after the fact.
You can see that same approach at work in my
<a href="/projects">projects</a>.
</p>

<p>
People often ask me: &ldquo;How can you code if you can&rsquo;t see
the computer?&rdquo; Everyone has limitations. The difference is what
we do with our abilities. I don&rsquo;t use a monitor and a mouse
- I use a keyboard and a screen reader. That&rsquo;s not a
limitation on what I can build. It&rsquo;s just a different
interface.
Page Explorer and VSCode&rsquo;s accessible view are both, at their
core, AI describing an interface to me. The next step is AI that
doesn&rsquo;t just describe an interface &mdash; it acts on one, and
coordinates with other AI to do it. That&rsquo;s the question behind
the research I&rsquo;m doing now: how agents behave, and misbehave,
when several of them have to work together. You can follow that work
on my <a href="/research">research page</a>.
</p>

<p><a href="/contact">Get in touch</a></p>
</main>
</BaseLayout>
99 changes: 99 additions & 0 deletions src/pages/projects/groupsio-group-manager.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
export const prerender = true;

import BaseLayout from '../../layouts/BaseLayout.astro';
---

<BaseLayout title="GroupsIO Group Manager — Blind Tech Mage">
<main id="main-content">
<h1>GroupsIO Group Manager</h1>

<p>
A WordPress plugin built for
<abbr title="Blind Information Technology Solutions">BITS</abbr>, a
blindness-focused nonprofit and professional affiliate of the American
Council of the Blind. It keeps BITS&rsquo;s Groups.io mailing lists in
sync with actual paid membership, automatically, so no one has to
manage list access by hand.
</p>

<h2>The problem</h2>
<p>
BITS runs its member communication over Groups.io mailing lists and
manages paid membership through Paid Memberships Pro (PMPro) on
WordPress. Before this plugin existed, keeping the two in sync was a
manual job &mdash; someone had to notice when a membership lapsed or a
new member joined, and update Groups.io by hand. Miss it, and either a
lapsed member keeps member-only access, or a new member goes
unreached.
</p>

<h2>What it does</h2>
<ul>
<li>
When a membership begins or changes, the plugin reads the mailing
list selections already captured on the member&rsquo;s PMPro
profile and adds them to the corresponding BITS Groups.io lists
&mdash; both the lists every member must belong to, and any tied to
their specific membership level.
</li>
<li>
When a membership ends &mdash; expiration, cancellation, or account
deletion &mdash; the plugin automatically removes the member from
every BITS Groups.io list they were part of.
</li>
<li>
A nightly reconciliation job checks for drift between what PMPro
says a member&rsquo;s access should be and what Groups.io actually
reflects, and corrects any mismatch.
</li>
<li>
A passwordless &ldquo;magic link&rdquo; feature lets a member reach
their WordPress profile by email, without needing to recall a
password.
</li>
</ul>

<h2>How it&rsquo;s built</h2>
<p>
Membership-change events are picked up via PMPro&rsquo;s
<code>pmpro_after_all_membership_level_changes</code> hook and handed
off to a background job queue (Action Scheduler), so a member checking
out or an admin making a change is never blocked waiting on a
Groups.io API round trip. Every add and remove call is logged to a
dedicated audit table, with its own retention policy. The Groups.io
API client handles rate limiting (HTTP 429, honoring
<code>Retry-After</code> with jitter) and retries transient failures
with exponential backoff, and hard-stops all sync activity with an
admin alert if the API credential itself becomes invalid.
</p>
<p>
Because a single member can be registered under different email
addresses for different mailing lists, sync and removal logic
operates on (list, email) pairs rather than assuming one address
covers a member&rsquo;s entire subscription. The plugin also honors
Groups.io&rsquo;s own unsubscribe, bounce, and spam signals &mdash;
once flagged, a member is permanently excluded from future automatic
adds, in keeping with CAN-SPAM &mdash; and handles GDPR/CCPA
right-to-be-forgotten erasure with an immediate, no-grace-period
removal from every list.
</p>

<h2>Admin controls</h2>
<p>
Administrators work through a &ldquo;GroupsIO Management&rdquo; admin
menu with three pages: feature controls (mandatory lists, grace
period before removal, log retention, and an emergency kill switch),
subgroup management (creating, editing, and inspecting the Groups.io
subgroups themselves), and user assignment (a searchable member list
with per-member details and manual override tools for when
automation needs a human to step in).
</p>

<p>
<a href="https://github.com/TechClusiveSolutions/GroupsIO-group-manager" target="_blank" rel="noopener noreferrer">View the source on GitHub<span class="visually-hidden"> (opens in a new tab)</span></a>
</p>

<p><a href="/projects">Back to projects</a></p>
</main>
</BaseLayout>
25 changes: 25 additions & 0 deletions src/pages/projects/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
export const prerender = true;

import BaseLayout from '../../layouts/BaseLayout.astro';
---

<BaseLayout title="Projects — Blind Tech Mage">
<main id="main-content">
<h1>Projects</h1>

<p>
A collection of the software I&rsquo;ve built &mdash; each one an
example of the same practice: real structure, real landmarks, real
semantics, not accessibility bolted on after the fact.
</p>

<ul>
<li>
<a href="/projects/groupsio-group-manager">GroupsIO Group Manager</a>
&mdash; a WordPress plugin that keeps a nonprofit&rsquo;s Groups.io
mailing lists in sync with paid membership status automatically.
</li>
</ul>
</main>
</BaseLayout>
Loading