Under the surface

How this site
is made.

The website is part of the workbench, too.

01 / The reason for making

A website to think with.

This is a place to share what I make, and a place to keep making. Writing, photographs and experiments sit together because each gives me a different way to follow an idea.

Some of the work is deliberately visible: a button on my desk changes a lamp on the homepage. Some is quieter: sending a smaller image, keeping text readable while a font loads, or letting a page arrive before its slower parts are ready. Both kinds of detail belong here.

Come and try a few experiments 

02 / The foundations

React, with Laravel.

One Laravel application brings the site together, enjoying the benefits afforded by Laravel Cloud. React handles the interface, and Inertia connects it to Laravel. I write articles in Markdown in my code editor, keeping the words close to the work.

Under the surface: rendering and content

Server-side rendering supplies HTML for a first visit. Once the browser takes over, Inertia can navigate between pages without reloading the whole document. Page components are split into separate JavaScript bundles.

Laravel reads the Markdown content and handles routes, validation and integrations. Draft access is authenticated independently of feature flags. Valkey holds small pieces of shared state, while Reverb carries live changes to connected browsers.

03 / Something you can see change

A real button. A shared change.

Feature flags let me change a particular behaviour without deploying the whole site again. Laravel Pennant provides that switch. In the conversation starter experiment, it controls fresh AI generation; when generation is off, prepared questions still give you something useful to try.

The homepage workbench lamp has its own flag. My LaMetric desk device can change it, and Laravel Reverb pushes the saved change to people already looking at the page. A small physical action becomes something shared on the web.

From the desk to the browser
  1. PressA physical button sends a signal.
  2. SaveLaravel changes the shared state.
  3. ShareReverb updates connected pages.
Under the surface: Pennant, Valkey and Reverb

Pennant uses a custom Valkey driver here. Valkey is a Redis-compatible key-value store, so these small flags can be shared across requests without adding a SQL lookup to the experiment. Updates are atomic: two requests cannot both read an old value and overwrite one another’s change.

The LaMetric button is stateless: it sends the same signal each time. Laravel decides the next value and saves it before broadcasting. The homepage reads its initial lamp state separately, after the page renders, with a loading placeholder while it waits.

Reverb and Laravel Echo deliver subsequent changes over a WebSocket connection. There is no repeated polling for the lamp’s state. On reconnection, the page reads a fresh snapshot; revision numbers prevent older messages from replacing newer state. The interface distinguishes a live connection from a saved snapshot.

See the workbench lamp  · Try the conversation starters 

04 / A little signal of activity

A counter on my desk.

The LaMetric also gives website activity a small physical presence: a number on my desk. The counter now uses the same managed Valkey resource as the feature flags, and Laravel supplies the display’s data in the format the device expects.

Under the surface: what the counter counts

This is an approximate total of successful public page requests, not a count of unique people. It stores a number, without building visitor profiles. Refreshes and bots can count. Prefetch, partial data requests, private pages and the device reading its own display do not.

Laravel increments the value during middleware termination, after the page response is sent under FastCGI. That keeps the storage operation off the response path, though it still uses server worker time. Pages served entirely from a browser or CDN cache do not reach the counter, and an outage can lose increments. Those limits make this a desk experiment rather than an analytics report.

The total is kept apart from disposable cache and has no automatic expiry. The historical total can be carried forward with a repeatable import that never lowers a larger saved count.

05 / Behind the scenes

Some work starts in a terminal.

Not every part of a website needs a button on a page. When I brought the traffic counter across, I needed a way to carry its existing total into the new site. A small command made that a repeatable operation.

These are Artisan commands—Laravel’s way of running application code from a terminal or the Cloud Commands panel. They use the application’s services and configuration, just as a web request can, but report their result back as text.

Under the surface: where commands live and how they run

This site’s counter command is written in routes/console.php. Its name, arguments and options describe the task; Laravel supplies the counter service, which does the storage work. Larger commands can also live in dedicated classes under app/Console/Commands.

php artisan list lists available commands. php artisan site-counter:seed --help explains the counter import’s arguments and options. Its default behaviour only creates an absent total. The --at-least option safely raises a smaller total, preserving any larger value already saved.

Running a command locally uses local configuration. Running it in Laravel Cloud’s Commands panel uses the selected deployed environment and its connected resources. Local and live data stay separate when those environments connect to different stores; pushing code does not copy the counter’s value.

A new command or option must be pushed and successfully deployed before Cloud can run it. The deployed command’s help output is a useful way to check which version is available. Commands run when invoked; recurring tasks need a scheduler configured separately.

06 / An invitation to play

AI, when you ask for it.

Conversation starters, BenBot and a haiku experiment give the Laravel AI SDK something concrete to do. They begin with a deliberate request from you, and stream words into the page as the response arrives. Simply opening or prefetching a page does not start a generation.

Under the surface: agents and streamed responses

Each task has an agent with instructions suited to the experiment. Laravel validates input, applies request limits and calls the model through the AI SDK. The application passes text deltas to React as they arrive, rather than waiting for the whole answer.

Provider credentials stay on the server. Prompts are sent to an external AI provider to generate a response. Incomplete responses and connection failures have visible error states; the conversation starter page also keeps its prepared questions available.

Meet BenBot  · Try a haiku 

07 / Keeping the character, losing the weight

Images at the right size.

The pencil portrait is an interpretation of a photograph taken by my daughter. Its soft edges and the paper around it are part of the design. Image optimisation needs to preserve that character while reducing what a browser has to download.

Under the surface: AVIF, WebP and responsive images

The portrait and selected local photographs are prepared in multiple sizes, with AVIF sources and WebP fallbacks. Responsive image hints let the browser choose a suitable file for the space and screen density. Existing photographs also continue to use Cloudinary and Flickr; Cloudinary delivery includes resized, format-optimised variants.

Image dimensions reserve space before the files arrive, reducing layout movement. The main portrait loads eagerly with high fetch priority, while lower-priority images can load lazily. The workbench lamp uses small responsive WebP artwork with its light drawn as an overlay, rather than downloading a new photograph whenever the flag changes.

Play with image transformations 

08 / The people behind the pixels

Circe Slab A. A story in the letters.

Making leaves a mark.

The headings and body text on this site are set in Circe Slab A. I want to celebrate the people and the craft behind the things I use, including the letters we read almost without noticing.

Circe Slab grew from the geometric sans-serif Circe. Alexandra Korolkova designed its upright styles with assistance from Olexa Volochay; Paratype released them in 2018. Its italics followed in 2021, the work of Korolkova, Maria Kharlamova and Alexander Lubovenko. Paratype tells the design story 

Korolkova is also a book designer and a teacher through her writing on typography. Her work includes leading the PT Sans and PT Serif project, and she received the Prix Charles Peignot in 2013. Meet the designer 

A longer thread through communication.

Paratype’s roots reach back to the type department of ParaGraph in the late 1980s. It became an independent company in 1998, continuing a tradition of multilingual type design. Artists and programmers working together to give language a form: that is a story worth keeping alongside the font. Paratype’s account of its beginnings 

A photograph made by my daughter. Letters shaped by type designers. Software built on other people’s careful work. This site carries those contributions with it.

Under the surface: smaller fonts, readable text

Circe Slab A and DM Mono are served locally as WOFF2 files. Small subsets contain the characters used most often; CSS unicode ranges let extended characters load from the larger font files only when needed.

The main reading font’s subset is preloaded. Other weights load as the page needs them. With font-display: swap, fallback text stays readable while the chosen typeface arrives. Browser checks cover both the small initial downloads and characters arriving later from external content.

09 / Making room for the reading

A head start on the next page.

After the initial page loads, the site prepares its main destinations in the background: both the page data and the React code. That happens without waiting for a hover. Individual articles load when you choose to read them.

On connections reporting data-saving or very slow network settings, that extra preparation is skipped. Photography and timeline pages can show their structure before external content is ready, with loading states rather than an empty wait.

Under the surface: prefetch, deferred data and caching

Navigation warming is deliberately bounded and sequential. It prepares a small set of destinations, not the whole archive. External photo and timeline data is excluded from that prefetch work. Deferred Inertia props fetch it after the page shell; the homepage timeline excerpt waits until it is near the visible part of the page.

External feeds use caching, timeouts and, where appropriate, a recent last-good result during temporary failures. Gallery images load in batches. Optional interactions and live-update libraries load when needed, keeping their work separate from the first page response.

These choices are checked with application tests, desktop and mobile browser tests, network inspection and local performance measurements. Local or simulated results are not claims about real visitors’ Core Web Vitals. Field measurement is a later step in understanding how the site behaves beyond my own tests.

10 / Back to the workbench

The next thing to try.

The physical button already reaches into the website. A next experiment is to connect a real office lamp to that same idea, so the digital and physical light can respond together. That connection is still to come.

I’m keeping notes as the site develops, including what works, what needs another attempt, and what might make a useful explanation for someone else. There is more to make, and more to learn from making it.

Explore the experiments  · Read the writing