2026-01-08 16:54:38
I’ve been writing a lot of Go in my new job, and trying to understand a new codebase.
When I’m reading unfamiliar code, I like to use print debugging to follow what’s happening. I print what branches I’m in, the value of different variables, which functions are being called, and so on. Some people like debuggers or similar tools, but when you’re learning a new language they’re another thing to learn – whereas printing “hello world” is the first step in every language tutorial.
The built-in way to do print debugging in Go is fmt.Printf or log.Printf.
That’s fine, but my debug messages get interspersed with the existing logs so they’re harder to find, and it’s easy for those debug statements to slip through code review.
Instead, I’ve taken inspiration from Ping Yee’s Python module “q”.
If you’re unfamiliar with it, I recommend his lightning talk, where he explains the frustration of trying to find a single variable in a sea of logs.
His module provides a function q.q(), which logs any expressions to a standalone file.
It’s quick and easy to type, and the output is separate from all your other logging.
I created something similar for Go: a module which exports a single function Q(), and logs anything it receives to /tmp/q.txt.
Here’s an example:
package main
import (
"github.com/alexwlchan/q"
"os"
)
func printShapeInfo(name string, sides int) {
q.Q("a %s has %d sides", name, sides)
}
func main() {
q.Q("hello world")
q.Q(2 + 2)
_, err := os.Stat("does_not_exist.txt")
q.Q(err)
printShapeInfo("triangle", 3)
}
The logged output in /tmp/q.txt includes the name of the function and the expression that was passed to Q():
main: "hello world"
main: 2 + 2 = 4
main: err = stat does_not_exist.txt: no such file or directory
printShapeInfo: a triangle has 3 sides
I usually open a terminal window running tail -f /tmp/q.txt to watch what gets logged by q.
The module is only 120 lines of Go, and available on GitHub. You can copy it into your project, or it’s simple enough that you could write your own version. It has two interesting ideas that might have broader use.
runtime packageWhen you call Q(), it receives the final value – for example, if you call Q(2 + 2), it receives 4 – but I wanted to log the original expression and function name.
This is a feature from Ping’s Python package, and it’s what makes q so pleasant to use.
This gives context for the log messages, and saves you typing that context yourself.
I get this information from Go’s runtime package, in particular the runtime.Caller function, which gives you information about the currently-running function.
I call runtime.Caller(1) to step up the callstack by 1, to the actual line in my code where I typed Q().
It tells me the “program counter”, the filename, and the line number.
I can resolve the program counter to a function name with runtime.FuncForPC, and I can just open the file and look up that line to read the expression.
(This assumes the source code hasn’t changed since compilation, which is always true when I’m doing local debugging.)
To use this file, I copy q.go into my work repos and add it to my .git/info/exclude.
The latter is a local-only ignore file, unlike the .gitignore file which is checked into the repo.
This means I won’t accidentally check in q.go or push it to GitHub.
It also means I can’t forget to remove my debugging code, because if I do, the tests in CI will fail when they can’t find q.go.
This avoids other approaches that would be more disruptive or annoying, like making it a project dependency or adding it to the shared .gitignore file.
[If the formatting of this post looks odd in your feed reader, visit the original article]
2025-12-31 21:33:19
I’ve read 54 books this year – a slight dip from last year, but still at least one book a week. I try not to set myself rigid targets, but I hope to reverse the downward trend in 2026.
I’m a bit disappointed in the books I read this year; compared to previous years, there were only a few books that I feel compelled to recommend. I’m not sure if it was bad luck or sticking too close to familiar favouites – but I can’t help notice that all of this year’s favourites are from new authors. That feels like a sign to look further afield in 2026.
What saved the reading year was community and connection. My a book club just passed its third anniversary, and the discussions are always a highlight of my month. In particularly enjoy the conversation if it’s a book we all liked – it’s more fun to celebrate what works than to tear a book to shreds. Two of my top picks below come from the book club list.
I also found some unexpected serendipity: Lizzie Huxley-Jones’s festive romance Make You Mine This Christmas has a meetcute in a bookshop in St Pancras station. I use the station regularly so I know the shop well, and it’s where my partner and I took our first photo together as a couple, at the beginning of our second date.
I track the books I read at books.alexwlchan.net, and writing the annual round-up post has become a fun tradition. You can see how my tastes have changed in 2024, 2023, 2022, and 2021.
Here are my favourite books from 2025, in the order I read them.

by Adrian Tchaikovsky (2024)
What if the robot apocalypse happened, but nobody told the robots?
We follow Charles, a robot butler who finds himself unexpectedly unemployed, and he travels through an apocalyptic wasteland to find a new purpose. In a world mostly devoid of humans, he struggles to find another household to serve. It’s a dark and absurd journey which I very much enjoyed, and the style reminds me of Douglas Adams. This world isn’t tragic, but absurd.
Beneath the lyrical and humorous style are messages about automation, class division, and our attitude towards work. The world is full of robots who are doing things because that’s Their Purpose, with no thought for who the automation is serving or whether it’s still necessary.
If you enjoy this, you should follow up by reading Human Resources, the prequel short story about a “human resources” department that only exists to fire all the humans.
I’d also recommend The Incomparable podcast’s Book Club episodes; I read Service Model on the strength of Jason Snell’s recommendation.

by Anita Kelly (2024)
A charming sapphic romance between a basketball teacher and a professional player.
Elle is a famous basketball player who’s proud and confident about being queer, but she struggles to be a foster parent to her niece, Vanessa. Julie is a capable high school coach who bonds well with her team, but feels unsure and uncertain about her queer identity. They both look up to the other, and are looked up to in return.
It felt like a very balanced romance, and I enjoyed our discussion of it at Ace Book Club. It hits all the classic sapphic tropes, and it has a feel-good ending.
My particular reading was enhanced by the annotations – my partner read this book first, and she highlighted passages with comments like “this seems familiar” or “remind you of anyone?”. Sadly that’s not a transferable experience, but I can tell you that I enjoyed it.
Surprisingly, I didn’t enjoy Anita Kelly’s other books. This book is the third in the trilogy, and I tried to read the other two – one of them was so-so, and the other I gave up on.

by Bea Fitzgerald (2024)
A sapphic retelling of the Trojan War, in which Cassandra’s curse is cast by a petty Apollo who just wants sex, and an enemies-to-lovers romance between Cassandra and Helen.
I really enjoyed this. It’s a well-written story and I enjoyed the first-person perspective of the two protagonists. It builds well towards its conclusion – a lot of stuff that becomes relevant later is established early and builds towards the end. Apollo’s curses on Cassandra, the gods forcing a narrative on Troy, how the story eventually deviates from the conventional myth.
The book has modern sensibilities, but retrofits them in a thoughtful way. It discusses consent, rape culture, and asexuality – in particular, Cassandra is implied to be ace – but never uses those words explicitly. These themes fit into the narrative, and don’t stand out as twenty-first century terminology or ideas shoved into Greek myth.
This was another book club pick, and I’m planning to read Bea Fitzgerald’s other books next year.

by Erin Edwards, Greg Callus, Rose Crossgrove, and others (2025)
This is the true story of Hester Leggatt, a woman who wrote fake love letters for Operation Mincemeat during World War II, then became a character in a hit musical.
Unlike the men in the story, almost nothing was known of Hester when SpitLip wrote the musical version of Operation Mincemeat, except that she wrote the fake love letters. Her character has the most emotional song in the show, but the fictional version had to be invented from scratch.
A group of fans were dissatisfied with this gap in history, and tracked down the real Hester. They traced the initial mistake to an interview that misspelt her name as Leggett instead of Leggatt. Once they knew the correct surname, they went through paper archives, old school records, even contacted MI5 – all to reconstruct her life story.
The book weaves this discovered history into a narrative, which is organised it into a coherent and readable story of Hester’s life – her place of birth, her career before and after the war, her love life, and even coincidental similarities to her fictional depiction.
I’m biased because several of those fans are dear friends, and I enjoyed watching their work from the sidelines – but I enjoyed reading the details even more so.
[If the formatting of this post looks odd in your feed reader, visit the original article]
2025-12-22 02:06:56
I recently read Ned Batchelder’s post about Truchet tiles, which are square tiles that make nice patterns when you tile them on the plane. I was experimenting with alternative headers for this site, and I thought maybe I’d use Truchet tiles. I decided to scrap those plans, but I still had fun drawing some pretty pictures.
One of the simplest Truchet tiles is a square made of two colours:
These can be arranged in a regular pattern, but they also look nice when arranged randomly:
The tiles that really caught my eye were Christopher Carlson’s. He created a collection of “winged tiles” that can be arranged with multiple sizes in the same grid. A tile can be overlaid with four smaller tiles with inverted colours and extra wings, and the pattern still looks seamless.
He defined fifteen tiles, which are seven distinct patterns and then various rotations:
The important thing to notice here is that every tile only really “owns” the red square in the middle. When laid down, you add the “wings” that extend outside the tile – this is what allows smaller tiles to seamlessly flow into the larger pattern.
Here’s an example of a Carlson Truchet tiling:
Conceptually, we’re giving the computer a bag of tiles, letting it pull tiles out at random, and watching what happens when it places them on the page.
In this post, I’ll explain how to do this: filling the bag of tiles with parametric SVGs, then placing them randomly at different sizes. I’m assuming you’re familiar with SVG and JavaScript, but I’ll explain the geometry as we go.
Although Carlson’s set has fifteen different tiles, they’re made of just four primitives, which I call the base, the slash, the wedge, and the bar.
The first step is to write SVG definitions for each of these primitives that we can reuse.
Whenever I’m doing this sort of generative art, I like to define it parametrically – writing a template that takes inputs I can change, so I can always see the relationship between the inputs and the result, and I can tweak the settings later. There are lots of templating tools; I’m going to write pseudo-code rather than focus on one in particular.
For these primitives, there are two variables, which I call the inner radius and outer radius. The outer radius is the radius of the larger wings on the corner of the tile, while the inner radius is the radius of the foreground components on the middle of each edge. For the slash, the wedge, and the bar, the inner radius is half the width of the shape where it meets the edge of the tile.
This diagram shows the two variables, plus two variables I compute in the template:
Here’s the template for these primitives:
<!-- What's the length of one side of the tile, in the red dashed area?
tileSize = (innerR + outerR) * 2 -->
<!-- How far is the tile offset from the edge of the symbol/path?
padding = max(innerR, outerR) -->
<symbol id="base">
<!--
For the background, draw a square that fills the whole tile, then
four circles on each of the corners.
-->
<g class="background">
<rect x="{{ padding }}" y="{{ padding }}" width="{{ tileSize }}" height="{{ tileSize }}"/>
<circle cx="{{ padding }}" cy="{{ padding }}" r="{{ outerR }}"/>
<circle cx="{{ padding + tileSize }}" cy="{{ padding }}" r="{{ outerR }}"/>
<circle cx="{{ padding }}" cy="{{ padding + tileSize }}" r="{{ outerR }}"/>
<circle cx="{{ padding + tileSize }}" cy="{{ padding + tileSize }}" r="{{ outerR }}"/>
</g>
<!--
For the foreground, draw four circles on the middle of each tile edge.
-->
<g class="foreground">
<circle cx="{{ padding }}" cy="{{ tileSize / 2 }}" r="{{ innerR }}"/>
<circle cx="{{ padding + tileSize }}" cy="{{ tileSize / 2 }}" r="{{ innerR }}"/>
<circle cx="{{ tileSize / 2 }}" cy="{{ padding }}" r="{{ innerR }}"/>
<circle cx="{{ tileSize / 2 }}" cy="{{ padding + tileSize }}" r="{{ innerR }}"/>
</g>
</symbol>
<!--
Slash:
- Move to the top edge, left-hand vertex of the slash
- Line to the top edge, right-hand vertex
- Smaller arc to left egde, upper vertex
- Line down to left edge, lower vertex
- Larger arc back to the start
-->
<path
id="slash"
d="M {{ padding + outerR }} {{ padding }}
l {{ 2 * innerR }} 0
a {{ outerR }} {{ outerR }} 0 0 0 {{ outerR }} {{ outerR }}
l 0 {{ 2 * innerR }}
a {{ innerR*2 + outerR }} {{ innerR*2 + outerR }} 0 0 1 {{ -innerR*2 - outerR }} {{ -innerR*2 - outerR }}"/>
<!--
wedge:
- Move to the top edge, left-hand vertex of the slash
- Line to the top edge, right-hand vertex
- Smaller arc to left egde, upper vertex
- Line to centre of the tile
- Line back to the start
-->
<path
id="wedge"
d="M {{ padding + outerR }} {{ padding }}
l {{ 2 * innerR }} 0
a {{ outerR }} {{ outerR }} 0 0 0 {{ outerR }} {{ outerR }}
l {{ 0 }} {{ 2 * innerR }}
l {{ -innerR*2 - outerR }} 0"/>
<!--
Bar: horizontal rectangle that spans the tile width and is the same height
as a circle on the centre of an edge.
-->
<rect
id="bar"
x="{{ padding }}" y="{{ padding + outerR }}"
width="{{ tileSize }}"
height="{{ 2 * innerR }}"/>
The foreground/background classes are defined in CSS, so I can choose the colour of each.
This template is more verbose than the rendered SVG, but I can see all the geometric expressions – I find this far more readable than a file full of numbers. This also allows easy experimentation – I can change an input, re-render the template, and instantly see the new result.
I can then compose the tiles by referencing these primitive shapes with a <use> element.
For example, the “T” tile is made of a base and two wedge shapes:
<!-- The centre of rotation is the centre of the whole tile, including padding.
centreRotation = outerR + innerR -->
<symbol id="carlsonT">
<use href="#base"/>
<use href="#wedge" class="foreground"/>
<use href="#wedge" class="foreground" transform="rotate(90 {{ centreRotation }} {{ centreRotation }})"/>
</symbol>
After this, I write a similar <symbol> definition for all the other tiles, plus inverted versions that swap the background and foreground.
Now we have a bag full of tiles, let’s tell the computer how to place them.
Suppose the computer has drawn a tile from the bag. To place it on the page, it needs to know:
From these two properties, it can work out everything else – in particular, whether to invert the tile, and how large to scale it.
The procedure is straightforward: get the position of all the tiles in a layer, then decide if any of those tiles are going to be subdivided into smaller tiles. Use those to position the next layer, and repeat. Continue until the next layer is empty, or you hit the maximum number of layers you want.
Here’s an implementation of that procedure in JavaScript:
function getTilePositions({
columns,
rows,
tileSize,
maxLayers,
subdivideChance,
}) {
let tiles = [];
// Draw layer 1 of tiles, which is a full-sized tile for
// every row and column.
for (i = 0; i < columns; i++) {
for (j = 0; j < rows; j++) {
tiles.push({ x: i * tileSize, y: j * tileSize, layer: 1 });
}
}
// Now go through each layer up to maxLayers, and decide which
// tiles from the previous layer to subdivide into four smaller tiles.
for (layer = 2; layer <= maxLayers; layer++) {
let previousLayer = tiles.filter(t => t.layer === layer - 1);
// The size of tiles halves with each layer.
// On layer 2, the tiles are 1/2 the size of the top layer.
// On layer 3, the tiles are 1/4 the size of the top layer.
// And so on.
let layerTileSize = tileSize * (0.5 ** (layer - 1));
previousLayer.forEach(tile => {
if (Math.random() < subdivideChance) {
tiles.push(
{ layer, x: tile.x, y: tile.y },
{ layer, x: tile.x + layerTileSize, y: tile.y },
{ layer, x: tile.x, y: tile.y + layerTileSize },
{ layer, x: tile.x + layerTileSize, y: tile.y + layerTileSize },
)
}
})
}
return tiles;
}
Once we know the positions, we can lay them out in our SVG element.
We need to make sure we scale down smaller tiles to fit, and adjust the position – remember each Carlson tile only “owns” the red square in the middle, and the wings are meant to spill out of the tile area. Here’s the code:
function drawTruchetTiles(svg, tileTypes, tilePositions, padding) {
tilePositions.forEach(c => {
// We need to invert the tiles every time we subdivide, so we use
// the inverted tiles on even-numbered layers.
let tileName = c.layer % 2 === 0
? tileTypes[Math.floor(Math.random() * tileTypes.length)] + "-inverted"
: tileTypes[Math.floor(Math.random() * tileTypes.length)];
// The full-sized tiles are on layer 1, and every layer below
// that halves the tile size.
const scale = 0.5 ** (c.layer - 1);
// We don't want to draw a tile exactly at (x, y) because that
// would include the wings -- we add negative padding to offset.
//
// At layer 1, adjustment = padding
// At layer 2, adjustment = padding * 1/2
// At layer 3, adjustment = padding * 1/2 + padding * 1/4
//
const adjustment = -padding * Math.pow(0.5, c.layer - 1);
svg.innerHTML += `
<use
href="${tileName}"
x="${c.x / scale}"
y="${c.y / scale}"
transform="translate(${adjustment} ${adjustment}) scale(${scale})"/>`;
});
}
The padding was fiddly and took me a while to work out, but now it works fine. The tricky bits are another reason I like defining my SVGs parametrically – it forces me to really understand what’s going on, rather than tweaking values until I get something that looks correct.
Here’s a drawing that uses this code to draw Carlson truchet tiles:
It was generated by your browser when you loaded the page, and there are so many possible combinations that it’s a unique image.
If you want a different picture, reload the page, or tell the computer to draw some new tiles.
These pictures put me in mind of an alien language – something I’d expect to see etched on the wall in a sci-fi movie. I can imagine eyes, tentacles, roads, and warnings left by a long-gone civilisation.
It’s fun, but not really the tone I want for this site – I’ve scrapped my plan to use Truchet tiles as header images. I’ll save them for something else, and in the meantime, I had a lot of fun.
[If the formatting of this post looks odd in your feed reader, visit the original article]
2025-12-20 06:57:16
I was creating a new S3 bucket today, and I had an idea – what if I add a README?
Browsing a list of S3 buckets is often an exercise in code archeology. Although people try to pick meaningful names, it’s easy for context to be forgotten and the purpose lost to time. Looking inside the bucket may not be helpful either, if all you see is binary objects in an unknown format named using UUIDs. A sentence or two of prose could really help a future reader.
We manage our infrastructure with Terraform and the Terraform AWS provider can upload objects to S3, so I only need to add a single resource:
resource "aws_s3_bucket" "example" {
bucket = "alexwlchan-readme-example"
}
resource "aws_s3_object" "readme" {
bucket = aws_s3_bucket.example.id
key = "README.txt"
content = <<EOF
This bucket stores log files for the Widget Wrangler Service.
These log files are anonymised and expire after 30 days.
Docs: http://internal-wiki.example.com/widget-logs
Contact: [email protected]
EOF
content_type = "text/plain"
}
Now when the bucket is created, it comes with its own explanation. When you open the bucket in the S3 console, the README appears as a regular object in the list of files.
This is an example, but a real README needn’t be much longer:
This doesn’t replace longer documentation elsewhere, but it can be a useful pointer in the right direction.
It’s a quick and easy way to help the future sysadmin who’s trying to understand an account full of half-forgotten S3 buckets, and my only regret is that I didn’t think to use aws_s3_object this way sooner.
[If the formatting of this post looks odd in your feed reader, visit the original article]
2025-12-17 17:48:51
A while ago I was looking for a palm tree emoji, and the macOS Character Viewer suggested a variety of other characters I didn’t recognise:

Some of the curves look a bit like Hebrew, but it’s definitely not that alphabet. I clicked on the first character (𐡱) and learnt that it’s Palmyrene Letter Pe, which is from the Palmyrene alphabet. I’d never heard of Palmyrene, so I knew I was about to learn something.
These letters are part of the Palmyrene Unicode block, a set of 32 code points for the Palmyrene alphabet and digits. One of the cool things about Unicode is that the proposals for new characters are publicly available on the Unicode Consortium website, and they’re usually pretty readable.
Proposals have to provide some background on the characters they’re proposing. Here’s the introduction from the original proposal in 2010:
The Palmyrene alphabet was used from the first century BCE, in a small independent state established near the Red Sea, north of the Syrian desert between Damascus and the Euphrates. The alphabet was derived as a national script by modification of the customary forms that cursive Aramaic which themselves developed during the first Persian Empire.
Palmyrene is known from documents distributed over a period from the year 9 BCE until 273 CE, the date of the sack of Palmyra by Aurelian. […] No documents on perishable materials have survived; there are a few painted inscriptions, but many inscriptions on stone.
Here’s an example of a funerary stone inscribed with Palmyrene script, whose shapes match the Unicode characters I didn’t recognise:

The proposal was written by Michael Everson, a prolific contributor who’s submitted hundreds of proposals to add characters to Unicode. His Wikipedia article lists over seventy scripts. He was profiled by the New York Times in 2003 – seven years before proposing Palmyrene – which described his work and his “crucial role in developing Unicode”.
He takes a very long view of his work. Normally I’m sceptical of claims about the longevity of digital work, but Unicode is a rare area where I think it might just last:
“There’s satisfaction in knowing that the work of analyzing and encoding these languages, once done, will never need to be done again,” [Everson] said. “This will be used for the next thousand years.”
And I liked this part at the end:
He likes to tell about how he met the president of the Tibetan Calligraphy Society at a Unicode meeting in Copenhagen. Mr. Everson had helped the organization ensure that Tibetan was included in the standard. The president showed Mr. Everson how to write his name in Tibetan with a highlighter pen.
“He thanked me,” Mr. Everson said with reverence. “I couldn’t believe that, because his organization has been in existence for over a thousand years.”
I spent eight years working in cultural heritage and thinking about the longevity of digital collections, but I never gave much thought to the history or encoding of writing. This is cool and important work, and I should learn more about it.
Palmyrene has 22 letters in its alphabet, which expands to 32 Unicode codepoints when you include alternative letters, numbers, and a pair of symbols.
The only letter I recognise is aleph (𐡠), which looks similar to the Hebrew letter aleph ℵ. I know the latter because it’s used by mathematicians to describe the size of infinite sets. It turns out aleph or (alef) is the name of letters in a variety of languages, not all of which look the same – including Phoenician (𐤀), Syriac (ܐ), and Nabatean (𐢁/𐢀).
The other letters have names which are new to me, like heth (𐡧), samekh (𐡯), and gimel (𐡢).
One especially interesting letter is nun, which appears differently depending on whether it’s in the middle of the word (𐡮) or the end (𐡭). This reminds me of the ancient Greek letter sigma, which is either σ or ς. I can’t help but see a passing resemblance between final nun and final sigma, but surely it’s a coincidence – the rest of the alphabets are so different.
The Palmyrene numbers look similar to the Arabic numerals we use today, but not necessarily the same meaning. One, two, three and four are regular tally marks (𐡹, 𐡺, 𐡻, 𐡼). The more unusual characters are five (𐡽), ten (𐡾), and twenty (𐡿) – but again, it’s surely a coincidence that the latter resembles the modern digit 3.
Alongside the letters and numbers, there are two decorative symbols for left/right fleurons (𐡷/𐡸).
Palmyrene is written horizontally from right-to-left, which introduced some new challenges while writing this blog post.
The first issue was in my text editor, which is fairly old and doesn’t have good right-to-left support.
I can include Palmyrene characters directly in my text, but it messes up the ordering and text selection.
I can navigate the text with the arrow keys, but it behaves in weird ways.
To get round this, I used HTML entities in all my source code (for example, 𐡠).
The second issue was in the rendered HTML page, where the Unicode characters affect the ordering on the page. In particular, I wanted to show the characters for 1, 2, 3, 4, in that order, so I wrote the four entities – but the browser uses a bidirectional algorithm and renders the sequence of characters as right-to-left. That’s the opposite of what I wanted:
| HTML: |
𐡹, 𐡺, 𐡻, 𐡼
|
|---|---|
| Output: | 𐡹, 𐡺, 𐡻, 𐡼 |
The fix was to wrap each character in the bidirectional isolate <bdi> element.
This tells the browser to isolate the direction of the text within that element, so the direction of each character doesn’t affect the overall sequence.
This gave me what I wanted:
| HTML: |
<bdi>𐡹</bdi>, <bdi>𐡺</bdi>, <bdi>𐡻</bdi>, <bdi>𐡼</bdi>
|
|---|---|
| Output: | 𐡹, 𐡺, 𐡻, 𐡼 |
This is the first time the <bdi> element has appeared on this blog, and I think it’s the first time I’ve used it anywhere.
I took the original screenshot in September. It took me three months to dig into the detail, and I’m glad I did. This is a corner of history and writing that I’d never heard of, and even now I’ve only scratched the surface.
The Palmyrene alphabet is an example of what I call a “fractally interesting” topic. However deep you dig, however much you learn, there’s always more to uncover.
[If the formatting of this post looks odd in your feed reader, visit the original article]
2025-12-12 18:30:02
I’ve been building a scrapbook of social media, a place where I can save posts and conversations that I want to remember. It has a nice web-based interface for browsing, and a carefully-designed data model that should scale as I add more platforms and more years of my life. As I see new things I want to remember, it’s easy to save them in my scrapbook.
But what about everything I’d saved before?
Across various disks, I’d accumulated over 150,000 posts from Twitter, Tumblr, and other platforms. These sites were important to me and I didn’t want to lose those memories, so I kept trying to back them up – but those snapshots had more enthusiasm than organisation. They were chaotic, devoid of context, and difficult to search – but the data was there.
After so many failed attempts, my scrapbook finally feels sustainable. It has a robust data model and a simple tech stack that I hope will last a long time. I wanted to bring in my older backups, but importing everything wholesale would just reproduce the problems of the past. I’d be polluting a clean space with a decade of disordered history. It was finally time to do some curation.
I went through each post, one-by-one, and asked: Is this worth keeping? Do I want this in the story of my life? Is this best left in the past?
That’s why I started looking back over fifteen years of a life lived online, which became an unexpectedly emotional meeting with my younger self.
One thing I’d forgotten is how much I learnt from being online, especially in fannish spaces. My timeline taught me about feminism and consent; about disability and the barriers of the built world; about racism in a way that went far deeper than anything I’d encountered before. I could learn about issues directly from the people who faced them, not filtered through a journalist’s lens. Today I take that social awareness for granted, but the Internet is where it started.
Social media was a crash course in humanity – broader, richer, and more diverse than anything I got from formal education.
Once I learned to shut up and just listen, Twitter let me follow conversations between people whose lives were nothing like mine. I got the answers to so many questions I’d never even known to ask, and I miss that. I stopped using Twitter after it was bought by Elon Musk, and I have yet to find another platform that replicates that passive, ambient learning.
More than anything else I saw online, queer culture has shaped my life. I’m queer, my partner is queer, and so are most of my friends. There are so many people I’d never have met if social media hadn’t introduced me to this world.
When I was realising I was queer, it all felt very difficult and angsty. Looking back, I can see myself following a classic path – talking to queer people, being a loud and enthusiastic ally, then starting to realise there might be a reason I cared so much. I went through it once when I realised I wasn’t straight, and again a few years later when I realised I wasn’t cis.
My younger self was oblivious, but it’s all so obvious in hindsight. I cringe at some of those older posts, but they helped me become who I am today, and I want to keep them.
There are other posts I look back on with less fondness. I’m embarrassed by how annoying I was when I was younger. I spent too much time on self-indulgent moralising and pointless arguments, often with people I probably agreed with on almost everything else. I wanted to be right more than I wanted to listen, and that got in the way of useful conversations.
Those arguments were worthless then and they’re worthless now. Deleting them was a relief.
Among my less admirable behaviour was the performative outrage toward the “main character” of the day – the unlucky person whose viral tweet had summoned thousands of replies explaining why they were a terrible person. Looking back, it was a symptom of misplaced familiarity. I was reading a stranger’s posts as if I knew them, projecting motives from scraps of context, and joining dogpiles to fit in with the crowd.
Despite ruffling a lot of feathers, I was only the main character once, and in a small corner of the tech community. It was still an unpleasant weekend, and I got off lightly compared to some of my friends – but I’ve never forgotten how quickly online attention can turn to anger and hostility.
Learning about parasocial relationships helped me behave better. I realised how often my reactions were shaped by a false sense of intimacy, and how easy it was to be cruel when I forgot there was a person behind the avatar. I shifted my attention towards friends rather than strangers, and when I did talk to people I didn’t know, I tried to be constructive instead of showing off.
When I joined Twitter, I admired by people who were smart. Today, I look up to people who are kind. I’ve come to value generosity and empathy far more than cleverness and nitpicking.
Looking through old conversations, I see the ghosts of friendships and relationships I’ve since lost. Some of those could be recovered if either of us reached out; others are gone for good. A few people have even passed away. I don’t know where most of those friends ended up, but I hope life has been kind to them.
I’ve passed through so many spaces: the PyCon UK community; fandoms like the Marmfish and the Creampuffs; the trans elders who supported me during my transition; the small, loyal group of blog readers who always left thoughtful comments. Some I lost touch with while I was still on Twitter; others I left behind when I left Twitter altogether.
As my interests changed and I moved from one space to another, I often did a poor job of keeping up the friendships I already had. I’d pour my energy into chasing new connections in the spaces I’d just discovered, neglecting the people who had been there all along. That neglect is stark when I look at it over a decade-long span. It was sobering to realise how many more friends I might have today if I hadn’t taken so many past connections for granted.
I’ve tried to keep lingering traces of those friendships by saving my mentions as well as my own tweets. Here’s one I found that made me cry: “One of the things I miss the most from my pre-pandemic Twitter timeline is seeing @alexwlchan traveling on trains and taking train selfies”. I miss that culture too – selfies were such a source of joy and affirmation, especially in queer and trans spaces. I miss seeing pretty pictures of my friends, and sharing mine in return.
When Elon Musk bought Twitter, a lot of my remaining connections there were broken. Some friends went to other platforms; others left social media entirely. I was one of them! I still write here, but it’s a more professional, broadcast space – it’s not a back-and-forth conversation.
I miss the friendships I had, and the ones that might have been.
I started with 150,000 fragments, which I reduced to 4,000 conversations. A lot of it I was glad to forget, but there are gems I want to remember. I’m glad I’ve done this, and it reinforces my belief that social media is an important part of my life that I should preserve properly.
Curating these memories has made them feel smaller and more manageable. The mess of JSON files scattered across disks has been replaced by a meaningful, well-organised collection I can look back on with a smile.
My use of Tumblr fell away gradually, and I stopped tweeting when Elon Musk bought Twitter. I didn’t jump to another platform immediately, because I wanted to pause and reflect on what I wanted from social media. Currently, my social media usage is limited to linking to blog posts.
Looking back over my old posts has helped with those reflections. I’d like to think I’ve grown up a bit in the interim, and that I’d use it better if I made it a bigger part of my life again. A lot of good things started as conversations on social media, and I often wonder if I’m missing out. But I don’t miss the time sunk in pointless arguments, the performative anger, or the abuse from strangers.
I still don’t know what my future with social media will look like, but this project has me wondering. Until I decide, my scrapbook lets me see the best of what’s already been – the friendships, the joy, and the moments that mattered.
[If the formatting of this post looks odd in your feed reader, visit the original article]