Skip to content

V3 - #3

Merged
JAlbertCode merged 38 commits into
mainfrom
v3
Sep 7, 2026
Merged

V3#3
JAlbertCode merged 38 commits into
mainfrom
v3

Conversation

@JAlbertCode

Copy link
Copy Markdown
Owner

No description provided.

JAlbertCode and others added 30 commits September 5, 2026 20:25
The portfolio links out to a lot of platforms that are not ours: articles,
videos, side-project deployments. Nothing told us when one of those went
away. This adds a zero-dependency checker that walks every external URL,
public asset and internal route referenced from app/, lib/ and components/,
plus a weekly Action that files a rolling issue when something rots.

YouTube is checked through oEmbed rather than the watch page, because a
removed or private video still returns 200 with an error page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
V2 was one page: a paragraph of bio, a row of social icons, and 37 project
cards behind a free-text skill filter. That filter had 221 distinct tags
across those 37 projects and 130 of them appeared exactly once, so it was a
list nobody could use. Every visitor got the same undifferentiated wall
whether they were booking a speaker, hiring, or looking for someone to build
something.

V3 asks who you are and takes you there.

Structure
  /            identity, four intent lanes, featured work, two ways to browse
  /work        the whole catalogue behind three combining filters
  /work/[slug] write-ups, generated from data
  /speaking    formats, topics, and every talk and stream with video
  /services    advisory, custom development, 3D printing
  /about       what the work is made of, counted from the catalogue, plus resume

Content model
  Everything is an Entry now: builds, talks, streams, and writing share one
  shape. The two collection pages that hid ten Spaces and eleven streams
  behind a single card are gone; each is its own entry, which is why the
  catalogue went from 37 to 56 pieces without anything being invented.

  Three controlled vocabularies replace the 221 tags. Form (what it is),
  practice (what I was doing), field (what it was about). Within an axis the
  selections are OR'd, across axes they are AND'd, and the whole filter state
  lives in the URL so any filtered view is a link you can send. Specific
  technologies stay on the entry as free text: displayed and searched, never a
  filter chip.

  Twenty hand-written project pages became twenty records in writeups.ts
  rendered by one route. The prose is unchanged apart from markup; five pages
  whose copy was clearly generated are marked needsRewrite.

Craft
  Design system built on CSS custom properties, dark and light, following the
  system preference with a toggle that persists and applies before first paint.
  MUI is gone; it was pulling emotion and the full component runtime to render
  three elements.

  Cards no longer autoplay video. V2 autoplayed ten muted clips in the grid,
  several over 50MB, which is most of a gigabyte pushed at anyone opening the
  home page on a phone. Now the poster loads and the video is fetched only when
  someone presses play.

  Fonts are self-hosted from @fontsource rather than fetched from Google, so
  the build has no network dependency and no visitor's browser calls Google.

  The play button is a sibling of the card link rather than nested inside it,
  every page has a skip link, focus is visible throughout, and result counts
  are announced.

Compatibility
  All 21 old project URLs 301 to their new home under /work, and the two
  collection pages redirect to the equivalent filtered listing. Nothing that
  was linked externally breaks.

  V2 files this replaces are moved to portfolio/_v2-removed/, which is
  gitignored. Delete that folder whenever you like.

Next: Next 16, React 19, Tailwind 4. Run npm install before npm run dev.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Checked against YouTube's oEmbed endpoint: the Moddio entry linked to
OsfmODTBQaA, which is "Farworld: Turning Chat Apps into Gaming Adventures",
and the Farworld entry linked to 8wQHD2qj8bs, which is "Game Dev Made Easy:
Fast Tools, Fun Creations, Funded Dreams" and matches the Moddio description.
They were swapped in V2 and the migration carried it forward.

Both videos are live, so nothing flagged this. A link checker cannot catch a
working link that goes to the wrong thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The first V3 skin was generic: dark ground, one teal accent, serif headings.
That is the default for a technical portfolio and it says nothing about whose
site it is. /lab puts four real identities against the real catalogue, each at
two intensities, so the choice is made from working pages rather than
descriptions.

  workshop  technical drawing. Registration marks, part numbers, dimension
            lines, a title block. The one language that joins the AR,
            hardware, and 3D printing work to the software.
  terminal  the site as a working environment. A prompt, a real query line,
            results printed rather than arranged.
  zine      his own publication. Masthead, thick rules, numbered entries,
            duotone covers, one loud spot colour.
  arcade    colour-coded by field. Each of the nine fields keeps its colour
            everywhere, so the catalogue is scannable by hue rather than by
            reading every label.

Intensity is a modifier rather than a rebuild: [data-loud] shifts ground,
saturation, type scale, and motif density on the same structure.

Also adds the connect surface, because the site is the destination for a
business card tap. On a phone the first screen is what someone actually wants
ten seconds after meeting Jay: save the contact, open a conversation, follow.
/contact.vcf serves a vCard built from lib/site.ts, sent inline so iOS opens
the add-contact sheet instead of dropping a file into Downloads.

Nothing here touches the live site. lab.css is scoped to [data-dir], the extra
faces live in lib/lab-fonts.ts, and SiteChrome hides the site header and footer
under /lab so each direction is judged on its own. All of it is deleted once a
direction wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The four art-directed skins missed. The feedback that mattered was that the
structure and functionality are right and the current site's virtue is being
fast and plain, so this stops redesigning the architecture and fixes only the
surface, aiming at plain executed well rather than at another concept.

Three decisions carry it.

The card is gone. A bordered, rounded, shadowed rectangle is the most
template-looking object on the web and is most of why the first V3 read
generic. Structure is hairlines, alignment, and space instead.

The work is a text list rather than a grid. Fifty-six entries in a grid is a
wall nobody reads; as a list it is scannable in one pass, weighs almost
nothing, and is real crawlable text, which matters because search and social
bios are entry points alongside the NFC card. Covers appear on hover, following
the pointer, one rAF-throttled transform on a single element and no library.
On touch the cover sits in the row instead and the hover code never runs.

Ordering follows who actually arrives. Someone from a search result or a bio
link does not know who Jay is, so identity comes first and the connect block
sits directly under it rather than in front of it. Someone who just tapped the
card still reaches it inside one thumb scroll.

One family throughout, Geist and its mono companion, with tabular numerals
wherever anything is counted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
V4 is the first V3's visual language with the Quiet direction's flow. Cards,
radius, and the teal accent come back because that part worked. Ordering and
density come from Quiet: identity first, connect directly beneath, six cards,
then the long tail as rows rather than a second wall of images.

Type is the open variable, so it is set with [data-type] and four pairings can
be compared on the same page instead of argued about by name. All four avoid
Inter and Instrument Serif, which is the pair the first V3 used and the pair
that read as default.

  a  Fraunces over Instrument Sans   most opinionated
  b  Bricolage over Schibsted        all sans, contemporary
  c  Newsreader over Geist           quietest editorial pairing
  d  Instrument Sans throughout      one family, no serif anywhere

Filler is now a standing rule, applied here and to Quiet:

  - "open to conversations" removed. He is obviously open to conversations.
  - "Go" and "View" removed from cards that are entirely links already.
  - "Start where you are" and "Browse" removed; the four kickers and the two
    column headings already say it.
  - "all catalogued below" removed. The reader is already below.
  - Connect sublabels that restated their own label are gone. "Fastest way to
    reach me" under a button marked Telegram carried nothing, so Telegram now
    shows the handle instead, and Book 15 minutes shows nothing. The vCard
    keeps its sublabel because a vCard is the one action whose behaviour is not
    obvious from its name.
  - The Quiet footer was a stats line restating the page. Removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The phone header was broken, not just crowded: the name wrapped to two lines
and the booking button was clipped outside the viewport. Past that, a visitor
scrolled thirteen controls before reaching a single piece of work.

Target size was never the problem. Everything here clears 48px against the 24
by 24 CSS pixels WCAG 2.2 SC 2.5.8 asks for at AA. The problem was how many of
them there were.

Removed rather than rearranged:

  - The mobile nav. Its four links are the same four destinations as the lane
    cards one screen below, named worse. Keeping the lanes means the phone
    needs no nav, no hamburger, and no disclosure state.
  - The header booking button, which repeated Book inside the connect block.
  - The entry count and the date span from the meta line. The list states the
    count and the entries state the span. Only the location survives, because
    it changes whether an event organiser can use him.
  - The running index on the work rows, which counted nothing anyone needs
    counted.
  - The "Also recent" label. The rule above it already separates the sections.

Compressed rather than removed:

  - Telegram, Email and Book go side by side, icon over label, three across.
  - The four socials go icon-only in one row. Each anchor carries an aria-label
    since the visible text is gone.
  - The lanes become two by two.
  - Chevrons now appear only on the primary action, where they mean something.

Eight stacked connect rows became three. "Book 15 minutes" is now "Book", which
fits one line beside a calendar icon that already says what it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Everything cut from it was duplicating something already on the page.

The vCard behind Save my contact carries the email, the Telegram handle and
every social URL, so the row of contact buttons directly beneath it was
offering a second copy of what the button above had just handed over. Gone.

The four lane cards were the four nav links wearing more paint, and they cost
most of a phone screen. Gone; the nav is back in the header, one line, where it
takes a tenth of the space. Their mobile rules went with them rather than being
left behind as dead CSS.

Book a call is now text beside the button rather than a button of its own.
There is one primary action on the page and it is the one an NFC tap is for.

The four socials are marks with no boxes and no labels, sharing the action row.

Thirteen controls above the first project became five, and on a phone the work
now starts inside one scroll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The cards they styled were removed in the previous commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Two mistakes, both corrected here.

Everything built over the last few rounds lived at /lab, so the real home page
never changed. The design is now on / and the shared tokens carry it to /work,
/speaking, /services, /about and the write-ups without those pages needing to
be touched.

The lab itself was scaffolding for presenting options, not something anyone
asked for. A visitor should never have seen a bar offering six design
directions and four type pairings. Gone, along with every direction that lost.

Deleted: app/lab, components/lab, lib/lab-fonts.ts, SiteChrome, IntentLanes,
and eleven font packages. 3,522 lines out, 329 in.

What shipped:

  Type      Newsreader for headings, Geist for body, Geist Mono for labels.
            Three faces, self-hosted, replacing Instrument Serif and Inter.
  Banner    A sentence, a button, four marks. The vCard already carries the
            email, the Telegram handle and every social URL, so nothing else
            belongs above the fold.
  Home      Six cards, then the rest as rows with the cover following the
            pointer on hover. Rows are crawlable text and weigh nothing.
  Header    Name, four links, theme toggle. The duplicate booking button and
            the hamburger both went.
  Cards     "View" removed. The whole card was already a link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The content was behind routes nobody had a reason to click. Nobody opens
"Speaking" to find out whether someone speaks, so twenty-eight sessions, three
services and the whole career section were effectively invisible to anyone who
was not testing the site.

Two routes remain: / and /work/[slug]. Work, speaking, services and about are
sections on the page a visitor already landed on, and the intent lanes jump to
them. The old paths redirect to their anchors, so anything already linking to
/speaking still arrives in the right place.

The filter moved with the work. Form and field chips sit inline above the
catalogue, so narrowing to talks about gaming is two clicks from landing rather
than a page away. Filter state is local rather than in the URL: on a single
page the URL carries the section anchor, and a filter competing for the same
address scrolls the reader somewhere they did not ask to go.

Also from this round:

  - No positioning statement. Any sentence short enough to sit under a name is
    narrow enough to pigeonhole the person under it, and fifty-six entries
    across nine fields prove a range no sentence could claim credibly.
  - The "Selected" heading is gone. The link beside it already said what the
    section was and how much more there is.
  - Telegram, Email and Book are separate actions again. Someone who wants to
    message him now should not have to save a vCard and open their contacts
    first. Save my contact is for keeping; those three are for using.
  - The intent lanes are back. They are not the nav wearing paint: the nav is
    named after pages, the lanes after what someone arrived wanting, which is
    the only label that lets a visitor self-select in one read.

Deleted: app/work/page.tsx, app/speaking, app/services, app/about, FacetBar,
WorkBrowser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The move is not settled, so the site should not be claiming it. Removed from
the line under the name, the page title, the meta description, and the TITLE
field of the vCard, which was the one place it would have followed someone into
their phone and stayed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The filtered view was the weakest thing on the site. Cards disappeared, so
filtering silently swapped the page for a plainer, worse one with no imagery
at all. Rows sprawled: a title on the left and a year eight hundred pixels away
on the right with nothing between them. Five zero-count field chips filled a
second row with options nothing could reach.

  - Rows carry their own thumbnail at every width now, so filtering narrows the
    catalogue instead of changing what kind of site this is.
  - Three columns: image, text held to a readable measure, metadata closing the
    right edge. The year sits with the row it belongs to.
  - The two filter axes are labelled and stacked. Nobody reads "Build, Talk,
    Stream, Writing, AI, Gaming" as one line and works out unaided that it is
    two different questions.
  - Field chips the current selection has ruled out are hidden rather than
    greyed. A chip nothing can reach is noise.
  - Clear is a real button instead of an underlined mono label.
  - Results fade in rather than snapping, and not at all under
    prefers-reduced-motion.

Two bugs found while reviewing:

  - An earlier edit spliced out the cta-ghost utility, so Telegram, Email, Book
    and both ghost buttons in Services and About had been rendering with no
    border at all. Restored.
  - The header stopped fitting a phone again: the name wrapped to two lines and
    the theme toggle was clipped past the right edge. The anchor links are
    hidden below 640px, where the intent lanes already cover them.

Also: covers now fail to a blank tile instead of a broken-image glyph, which
matters because four are still served from YouTube's CDN. The
pointer-following preview is gone, since every row shows its cover anyway and
it was holding fifty-six images in a hidden div. Header is a server component
again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Jay never gave a talk at GDC. The site said he did, in three places, because I
took V2's description of the Loot GDC article ("Technical demonstration of
on-chain lootboxes presented at Game Developers Conference") and hardened an
ambiguous, probably generated line into a specific claim: medium "talk", and a
summary asserting "a technical session at the Game Developers Conference on
implementing lootbox mechanics on-chain, and the fairness argument for doing
so". None of that was sourced. It was on a featured card.

The article says what actually happened. Loot GDC was a lootbox scavenger hunt
built and run across the GDC venue and online, with nine studios contributing
prizes and players creating wallets without noticing. His own line in it: "I
joined Chainsafe Gaming in January, and the Loot GDC experience was my first
big challenge." He built the campaign and wrote it up.

Corrected the entry to a build, and removed the same claim from the speaking
formats and from the ChainSafe role description. ETHDenver stays, because
YouTube's oEmbed returns author "ETHDenver" for that video.

Audited every other summary against V2's originals for claims I added rather
than carried. Three more, all softened: two Spaces called "a panel" when the
source never says how many people were on them, and an "onboard users to
MetaMask" that had become "their first MetaMask connection". Everything else
traces to V2's own descriptions, which are themselves partly generated and may
hold errors only Jay can catch.

entries.ts now opens with the sourcing rule: a summary may only state what the
linked artifact or Jay states, and V2's descriptions are not a source.

Separately, the site had been written as though developer relations were the
whole job. The catalogue says otherwise: product and design on eighteen
entries, community and education on thirteen, engineering on twelve, writing on
ten. The line under the name, the intro, the speaking topics and the advisory
pitch are all plural now.

Structure is three pages rather than one or five. One page was a very long
scroll; five hid the content behind labels nobody clicked. Home carries what
every visitor wants regardless of why they came, /services is for anyone who
wants to engage him, /about is for anyone deciding whether to hire him, and
both are previewed on the home page with enough substance to make the click
informed. Speaking is part of working with him, not a separate offer.

Also: "Save my contact" is "Add to contacts" with a line saying what a vCard
does, and the intent cards are gone. Four cards telling someone to jump to
sections beneath them was the page describing itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Two things were rendering as two different components when they were one.

Social links were bare icons in the hero and icon-plus-label in the footer.
There is one SocialLink now and only the set changes: the hero carries the four
worth following in the first ten seconds, the footer carries all of them. The
follow list is also derived from the single social list in site.ts rather than
retyped, because two hand-maintained copies of the same four links is how they
drift apart.

Field tags were bordered chips on cards and plain mono text on rows, which made
one catalogue look like two systems. Plain mono in both.

The header also had the theme toggle sitting beside the name instead of at the
far right, because the nav's auto margin was pushing past it. DOM order is name,
nav, toggle now, with the nav ordered last below 640px so it wraps to its own
row and the toggle stays up beside the name.

Print work now routes to Layerworks Print Co. rather than to email. The service
block is written from the shop itself: custom pieces from $35 with design
included, a quote inside 24 hours, a spinnable 3D preview to approve with the
price locked at that step, printed in-house and shipped free in the US. My
previous version described a process that did not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Telegram, Email and Book were bordered buttons while GitHub, LinkedIn, X and
Medium were plain, on a distinction between doing something now and subscribing
to something later. That rule was invisible. Telegram is a social platform that
got a box while X did not, and Email and Book are not social at all. Jay could
not see the logic, which means no visitor could either.

Weight now tracks importance rather than category. Add to contacts is the one
action an NFC tap exists for, so it is the only thing that looks like a button.
Everything else is a link that looks like every other link: Email, Telegram,
Book, GitHub, LinkedIn, X, Medium, one row, icon and label, same size and
colour throughout.

Email and Book needed glyphs to sit in that row without looking like the odd
ones out, so SocialIcon carries an envelope and a calendar alongside the
platform marks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Nobody taps 'Save my contact'. Someone who has just met Jay reaches for
Telegram, X or LinkedIn, so those lead the row now and the vCard route
is gone entirely.

Telegram is promoted to a primary social, which means it arrives through
followActions and no longer needs its own hard-coded entry in
talkActions. That leaves talkActions as the two slower channels, Email
and Book, and the row reads Telegram, X, LinkedIn, GitHub, Email, Book
with one visual treatment throughout.

WhatsApp's glyph is in SocialIcon ready for the number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The catalogue was 56 pieces of found imagery shown raw: partner stream
banners, bare logos on white, screenshots. It read as 56 other people's
brands, the white exports were the brightest thing on a near-black page,
and four covers were hotlinked from YouTube's CDN, one of which was
already rendering as a broken image on the home page.

scripts/build-covers.py now generates every tile: trim the exported
border, fit the whole thing inside 16:10 without cropping, centre it on
transparency so the card paints the ground and the tile is right in both
themes, then a small desaturation and a ceiling on luminance, lower again
for anything that is mostly a white plate. Entries still name the
original file; cards derive the tile, and the detail page shows the
artifact itself at its own aspect using dimensions the script records.

Six entries have no usable source: four whose only thumbnail lived on
YouTube's CDN, one exported at 246px, one fully transparent. Their tile
is empty rather than an upscale, a hotlink, or the title printed a second
time three lines above itself. Summary is optional for the same reason.

Also here: the detail page's action moves out of the sidebar and into the
reading column, so it stops reading as the artwork's branding on entries
whose artwork is turquoise; the hero closes on a rule instead of trailing
off; 'Work with me' becomes 'Services' so it stops sharing its first word
with the nav item beside it; and rows and cards fade in on first sight,
once, staggered in sixes.

Content, all verified against the source rather than written from the
title: 21 Midnight dev hangouts (titles and channel from YouTube's oEmbed
record, dates from the titles or the watch page's uploadDate), three
articles with Jay's byline on midnight.network, and six repositories from
the JAlbertCode account whose READMEs actually describe the project.
Whether Jay hosted or appeared is not written down anywhere public, so
none of the hangout entries say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The grid of cards above the list looked different from the rows for a
reason held in a boolean nobody could see, and the boolean had gone
stale: it was showing 2023 work at three times the size of last month's.
It is gone.

What replaces it is a rule a reader can act on. Entries that open into a
write-up here are set large and flagged; the sixty-five that bounce
straight out to someone else's page stay compact. Years are marked,
because eighty-six rows need somewhere to breathe and a year needs no
explaining. And above 1100px the row thumbnails give way to one frame
pinned beside the list that shows whatever the pointer is resting on, at
a size where you can actually see it.

The frame is only drawn when it has something in it, and rows with no
cover drop the thumbnail cell rather than leaving an empty square, both
because thirty of the newest entries have no image yet and an empty
bordered box reads as a component that failed.

That gap is the real limit on all of this: everything from September 2025
on has neither a cover nor a write-up, so the rhythm and the preview are
both invisible for the top third of the list.
scripts/fetch-youtube-covers.mjs closes most of it, downloading the
thumbnail for every YouTube entry that has no cover so it can be
committed and normalised like any other asset rather than hotlinked. It
needs network, which the sandbox does not have, so it has to be run on
Jay's machine. build-covers.py takes --all and reads its file list out of
entries.ts.

Also: the home page no longer prints his name in the header directly
above the same name set at four rems, in a different form of it. Every
other route keeps the wordmark, where it is the only thing saying whose
site this is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The opening viewport was a name, one sentence, six links and a hairline,
with the work starting below the fold. On the page someone reaches by
tapping a business card that is most of a screen of nothing, and no
amount of rearranging the list underneath was going to fix it. Restraint
and absence are not the same thing.

Two changes. The name is now set at the width of the page and treated as
the graphic, because there is no photograph or illustration on this site
and type is the only thing available to carry the top of it at scale. The
line and the links share a row beneath it instead of stacking down the
left edge.

Then the work starts immediately: a full-bleed band of the twenty-four
most recent covers, each one a link to the piece itself. It runs off the
right edge deliberately, since the point is that there is a lot of it,
and it scrolls rather than animating, because a strip that slides on its
own is a thing to wait for and it takes the tile you were about to click
away from you. Every tile is a real artifact through the same generator
as every other tile on the site, so this is the work, not decoration
standing in for it.

The home header also needed the nav to lead now that the wordmark is
gone, otherwise the theme toggle sat alone on the first row of a phone
with the nav underneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
The list was the wrong call. The work is visual, and a dense index of
titles throws away the only thing that makes a portfolio worth opening.
Cards, newest first, three across, everything the same size. No featured
grid above a list, no large-and-small split: one treatment, and the order
is the date.

The cover band goes with it, since it was showing the same images the
grid now shows one screen further down.

What the split was carrying, the card keeps: entries that open into a
write-up here are marked, because whether a click stays on the site or
leaves it is the one thing a card cannot show by looking at it.

Which leaves the images as the whole problem, so
scripts/fetch-covers.mjs now goes after every entry without one rather
than only the YouTube ones: a video's thumbnail where there is a video,
the page's og:image otherwise, downloaded into public/images and written
into entries.ts rather than hotlinked. That covers 35 of the 36 gaps
without anyone hunting for a file. It needs network the sandbox does not
have, so it runs on Jay's machine.

WorkRow, WorkPreview and CoverBand are removed rather than left
unreferenced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Every hangout entry now carries a summary condensed from that video's own
YouTube description, read off the watch page, with the promotional tail
and the link dump cut. All 86 entries have one.

What the descriptions still do not say is whether Jay hosted or appeared.
A few name him for a particular segment, most say nothing either way, so
no entry claims either.

He goes by Jay online and at work, so the site says Jay Albert
everywhere someone reads it. The full form survives as site.legalName,
used in the footer, which is the one place it belongs. And he still calls
it Twitter, so the social row does too.

The hero name was set at the width of the page. At eight rems it read as
shouting rather than as scale, so it comes back to something that still
carries the top of the page without being the only thing on it, and the
gap above the work closes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
Month precision put six things in August 2026 with nothing to separate
them, so they fell back to the order they happen to sit in the file, and
the storefront finished on the 31st sorted below a hangout from the 12th.
That is what Jay noticed.

date now takes yyyy-mm-dd as well as yyyy-mm. Both still sort as plain
strings, which is the whole reason for the format, and formatDate renders
the day when there is one. It builds the Date from the parts rather than
parsing the string, because new Date('2026-08-31') is UTC midnight, which
in New York is the evening of the 30th.

Thirty entries now carry a day. Repos take the last commit on the default
branch from the GitHub API, which is when the work was last actually done
rather than when the repo happened to be created: the leaderboard was
started in January and still being worked on in May. Articles take the
publish date printed on the page. Hangouts take the date in the title, or
the watch page's uploadDate for the three without one.

The remaining fifty-six are pre-2026 and keep the month they had, because
those dates are Jay's own record of when the work happened and a repo
push or an upload timestamp is not the same claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQQzUAbRStgw3ivKEj4tGy
build-covers.py crashed on lilypad-frontend.avif because that Pillow has
no AVIF support, after seventy-five tiles had already been written and
before the size map was saved. Now an undecodable file is reported and
skipped, the run continues, and cover-sizes.json is merged rather than
replaced, so a file this run could not read keeps the size a run that
could recorded for it. When the skipped files are AVIF it says so and
names the plugin.

fetch-covers.mjs got the headers a browser actually sends. A bare
user-agent gets a 403 from some hosts, which is why the three
midnight.network articles came back empty even though their og:image tags
are there and point at real 1920x1080 files on cdn.sanity.io.

It also says why each one failed now, instead of reporting nothing
The Services and About cards move above the catalogue. Underneath
eighty-eight cards they were past the point where anyone was still
looking for a link: by then the reader is scrolling the work, not hunting
for the rest of the site.

Covers for the two the fetcher could not reach, both from the project's
own repository rather than anything invented. The storefront has no
og:image at all, so this is a render of capsule_slab.glb, the featured
print, done offscreen with three.js at the model's own proportions.
Family Affairs had GitHub's generated social card, which is real but is
the same card five other repos ship; the deck's own character art is
better and is what it uses now.

Two articles that were missing. "The Internet Is the New Culture" from
the Medium profile, and "Partner sprints on Midnight" from
docs.midnight.network, both under Jay's byline, both with the publish
date printed on the page. The three Medium pieces already listed now
carry their exact dates from the same source.

fetch-covers.mjs gains --frames. Eighteen of the hangouts are one
recurring show and all eighteen ship the same green title card, which is
most of what makes the catalogue read as one thing repeated. YouTube
exposes three frames taken from inside every video, and those differ
episode to episode because the footage does. It defaults to the Fireside
episodes and takes explicit slugs otherwise, and it replaces an existing
cover rather than inserting a second one.

Also carries the tiles Jay's own build-covers run regenerated.
Jay's byline on docs.midnight.network, published 15 October 2025, which
is the same day as the Hacktoberfest Fireside Dev Hang already in the
catalogue.

Fourteen pieces of writing now. It has no cover yet; fetch-covers.mjs
will pick up its og:image on the next run.
…ir tile

The 3D printing card carried "custom pieces start at $35 with design
included, and you get a quote inside 24 hours". That is verbatim from the
commissions page of the Layerworks shop, so it was not invented, but it
was still wrong to put here: it is a commitment on Jay's personal site
that he did not agree to make, and two copies of the same commercial
terms drift the moment one is edited. The card now describes the work and
hands people to the shop, which is the only place that should state
terms. No price, no turnaround, no shipping.

The same doubt applies to the deliverables on the other two cards. Those
were written from what the work looked like, not from anything Jay
confirmed, and a bulleted list of what a buyer receives is a
specification of the job whether or not it is labelled one. Flagged in
place rather than quietly left.

The service buttons now line up. Each card used to be as tall as its own
content, so the three calls to action finished at three different heights
and the row had no baseline.

And the covers: Image.thumbnail only ever shrinks, so a source smaller
than the tile was left at native size. The in-video frames are 480x360,
which left every one of the eighteen hangouts as a small picture centred
in a large empty rectangle, and eighteen of those read as the same empty
rectangle, which is exactly what the frames were meant to fix. Small
sources now scale up: three times for a photograph or a screen capture,
which carries the detail, and only 1.8 for a flat mark, which does not.
Sorting now runs on the last time a thing changed rather than when it
first appeared. A repository started in January and still being pushed to
in May belongs above one finished in March, because there is something
new to look at. Entry.updated carries that where a source records it,
which for a repository is the last commit on the default branch; an
article or a recording does not get one, because nothing revised them.
Cards say Updated where it applies.

The detail rail is the same on every entry: same sections, same order,
one treatment. It used to render Practice and Field as plain links and
Built with as bordered mono chips, so one panel carried two visual
systems, and the whole Built with block vanished on entries with no tech,
which made the rail a different shape depending on which page you were
on.

All eighty-nine cards are on the page. Showing eighteen and hiding the
rest behind a button is a strange thing to do on a page whose entire
purpose is the catalogue: the heading says eighty-nine and then the page
shows you a fifth of it.

Services and About stopped being cards. They sit directly above
eighty-nine actual cards, and anything with a border and a hover lift
there is a card competing with the catalogue on its own terms and losing:
two odd tiles with no image at the top of a grid of tiles with images.
Now it is a rule, two columns of type and an arrow.

Summaries pulled into one band. They ran from 37 to 289 characters, which
is why the cards did not line up; fourteen were cut and nothing was
added, and eighty-one of the eighty-five now sit between 75 and 150.

fetch-covers.mjs carries resolved image URLs for the five articles whose
HTML refuses a plain fetch even with browser headers. The tags were read
out of the live DOM, so the URLs are the ones each page declares, they
just skip the step that keeps failing. It also survives an unreachable
host now instead of dying on an unhandled rejection.
fetch-covers.mjs wrote entries.ts with two regexes that started at a slug
and scanned forward with an unbounded lazy match. On an entry with no
cover the scan ran straight past the end of that entry and rewrote a
later entry's cover instead. night-and-dust-for-developers has no cover
and example-mip-0014 is the next entry along, so the article's image
landed on the reference implementation and the article kept nothing. That
is why MIP-0014 went blank and why four articles stayed empty after a run
that had already downloaded their images.

It now splits on the entry boundary first and patches one block at a
time, so no pattern can see outside the entry it belongs to. The four
already-downloaded covers are reassigned to the entries they belong to.

Cards line up. The fields line is pinned to the bottom of the card, and
the title is clamped to two lines. Clamping alone was not enough: a
two-line summary still leaves a gap a three-line one does not, so the
fields line floated up with it and the row lost its baseline. The Updated
and Write-up flags are separated from the fields, which they used to run
into as if they were one more field.

Services and About lost the mono count line. A display face, a body face
and a mono face in a block four lines tall is what read as a mesh rather
than as one thing. Two type styles now.
Both posts declare the same house banner, /img/blog/ecosystem.jpg, and
neither carries any other image, so downloading what they publish would
have put the same picture on two cards. The banner set has fourteen of
these and they are already icons: a line drawing on a light plate with
the section name in the corner.

So partner sprints keeps ecosystem, which is the image it publishes, and
Hacktoberfest takes hackathons from the same set. That is an editorial
choice of image rather than the one the post declares, which is worth
saying out loud, and it is not a claim about anything: same designed set,
same site, and the alt text says which icon it is.

The files came from raw.githubusercontent.com, which is one of the few
hosts the local VM can reach, so this needed no round trip.

Eighty-eight of eighty-nine entries now have a cover. The one left is
mix3d, which has no link to pull from and whose only image was a fully
transparent PNG.
JAlbertCode and others added 5 commits September 7, 2026 03:29
Family Affairs and the Layerworks storefront were cloned, installed and
built, then served and photographed headlessly. Those are the actual
interfaces: the join screen with the family artwork and the four-letter
room code, and the storefront with the capsule slab in its 3D viewer and
the dimension callouts beside it.

The Midnight Leaderboard is live on preprod, which its README says, so
that one is the deployed app: click challenge, real entries, real scores.

The storefront replaces the render of capsule_slab.glb I made from the
model file. That render was honest but it was a picture of one part; the
page is the work.

zk-leaderboard could not be built here. Its UI depends on the Midnight
packages and those did not resolve on this machine, so the deployment did
the job instead.
It listed Lilypad, ChainSafe and Mix3d and stopped, which meant the About
page omitted his current job. Nine roles now, from his own LinkedIn:
Midnight Foundation, OMM Games, Lilypad, ChainSafe, Godwoken, Smith +
Crown, Mix3d, OppenheimerFunds and The Fourth Branch.

Titles, employers and periods are exactly as they appear there, which
also corrects two that were wrong: Lilypad ran January to August 2025,
not 2023 to 2025, and ChainSafe ran into 2025. Each detail line is
condensed from the bullets on that entry and adds nothing to them.
Finalized home page
Updating calendar booking
Cleaned up code
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portfolio-v2 Ready Ready Preview Sep 7, 2026 8:16am UTC

Cleaned code
Additional cleaning
Updated checks
@JAlbertCode
JAlbertCode merged commit b4693bf into main Sep 7, 2026
3 checks passed
@JAlbertCode
JAlbertCode deleted the v3 branch September 7, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant