Redesigning My Homepage Without Changing a Word
I redesigned the homepage of this site this week, and the words on it are exactly the same as they were before. The hero headline and bio come from my author profile in Sanity, each section's title and description come either from page settings or from the strings the old components already used, and the order of the sections is still whatever I arrange in Sanity Studio. Only the layout changed, along with what the hero shows and how much of my work is visible on each screen.
Keeping the text fixed was a deliberate constraint. If I had rewritten the copy while changing the layout, I would not have been able to tell which of the two had improved the page, so the design had to be the only variable. It also meant the redesign could not take control of the content away from the CMS, which was the main thing I wanted to protect.
The old homepage was centred from top to bottom: an avatar with a glow, the headline, a long paragraph, a status card with rotating roles, and then ten sections of identical height, each revealed with the same fade as it scrolled into view. Each section also showed its own name twice, once as a small label and once as a heading directly below it. None of this was broken, but the first screen showed very little of the work itself, and the uniform sections made a writing archive, a poetry archive, and a contribution graph all feel like the same kind of thing.
The new hero is left-aligned, which gives the bio a comfortable reading measure and frees the right side for something that is not decoration. That space now holds a publishing record. Each column is one week of the last 52, each lit dot is one writing, dev note, or quick ship published that week, and the months are marked underneath. Below the graph are the number of pieces in that period and a link to the most recent one, with its date.
I chose the record because it is the only thing in the hero that changes on its own and can be checked. A visitor can follow the latest link, count the dots against the archive, or notice that a month is empty. The status card it replaced rotated through four roles every 2.4 seconds, which said something about me but nothing about the work, so it is gone entirely.
A graph like this is only honest if it never goes stale, so the most important part is the code that builds it. It reads writings, dev notes, and quick ships, drops anything dated in the future, and groups the rest into weeks that start on Monday in UTC. I kept that logic in a module with no data loading in it, so it can be tested directly:
Because the window is calculated from the current date each time the page is generated and is not stored anywhere, the graph moves on its own: old weeks fall off the left edge as new ones appear on the right, and a post I schedule for next Monday stays out of the count until Monday arrives. The tests cover exactly those cases, along with a new piece landing in the current week and becoming the latest. The homepage regenerates every 60 seconds, and the Sanity queries it depends on revalidate on the same interval, so a new piece reaches the graph within about a minute without a deploy.
The grid itself had to fit a panel whose width changes with the screen. My first instinct was a fixed pitch of 9 pixels per week, but 52 columns at 9 pixels needs 468 pixels, and the panel is often narrower than that. The grid now divides whatever width it has evenly between its columns and keeps each dot square with aspect-ratio, so the full year always fits. On phones it shows the most recent 26 weeks, and the month labels switch with it, so the axis never describes dots that are not on the screen.
Below the hero, every section is a ruled row with its heading on the left and a dense list on the right. The heading column is sticky on wide screens, so the name of a long section stays visible while you read through its entries. Each list shows six entries where the old cards showed three, with the date in a fixed mono column so the titles line up into a single scannable edge. Quick ships without a title use their opening text as the title, so every title is capped at three lines; otherwise one long post could stretch a row to several times the height of its neighbours.
The numbers above each heading are set in Geist Pixel Line, the scanline variant of the pixel typeface I use for Raster. It is the only pixel type on the homepage, and I kept it at 40 pixels because scanline letters stop reading as letters at small sizes. Its metrics are unusual: the glyphs rise 0.71em above the baseline and nothing falls below it, while the font's own box extends 1.01em up and 0.30em down. Setting line-height: 0.71 makes the line box end exactly on the baseline, so the number sits flush on the label beneath it with no empty space reserved for descenders the font does not have.
The section order still comes entirely from Studio. The loader reads the order, removes the sections that do not belong on the homepage, and inserts one new section when it has not been placed yet:
That new section links the homepage to my design principles page, which previously had no route in from the homepage at all. It shows three of the mockups from that page on their generated halftone backgrounds, followed by every principle as a link to its anchor. Its title and description come from the same Sanity document as the design page, so editing the page edits the homepage section too, and nothing about it is written twice.
The links under the hero were the last thing that looked unfinished. GitHub and X were plain text, the terminal link sat in its own bordered box with a pulsing green dot, and REPRINT had a pill beside it, so the row had three different visual weights across four items. They are now the same height and padding, separated by one-pixel dividers, with every item at least 44 pixels tall so it is easy to tap. The terminal's dot is still there, but it no longer pulses, because nothing about it is live enough to justify the motion.
The GitHub contribution graph scrolls sideways on narrow screens, and it was the one place where the browser's default scrollbar still appeared, grey arrows included. It now uses a thin bar drawn in the site's border colour with the arrow buttons removed, through scrollbar-width and scrollbar-color where they are supported and the WebKit scrollbar pseudo-elements elsewhere.
The old homepage was assembled from ten section components and the status card, and nothing else on the site imported them. Once the new layout replaced them, keeping them would have left a second, unused homepage in the codebase that anyone, including me, could mistake for the real one, so I deleted them. The new page is one component that renders every section from a single content object, which makes it straightforward to see where each piece of text on the homepage comes from.