Postecard

It’s been more than a year since Elon Musk bought Twitter. It got demonstrably worse this January when third-party apps were cut off, and the gradual decline has continued since then. The (many) ads I see are almost all from newly-created accounts for dropshipping companies, whether under their AI-generated storefront names, or AI-generated “real people” and their testimonials. Many of the actual real people I followed have left, most of the media sources I followed are still there and it’s still the most reliable and quick way for me to follow them, so I’m still on Twitter. I’m also on Mastodon, Bluesky, and Posts. I made a Threads account to reserve my handle. Too much, right?

Too much! So I also made a one-person Twitter app for myself. Yes, another Twitter clone, the worst one possible, to add to a roster of feeds of small text posts to refresh compulsively. The goal is for it to be the place where I post tweet-sized updates that are actually meaningful to me, and I will try to reduce my use of all the other places to only reading or not at all. And this new one is broadcast only, no replies or likes or other engagement — great for mental health! Followers can either go to my website to read the latest (and only the latest) tweet, or subscribe to an RSS feed of every tweet to automatically get new ones and go through the old ones.

I call it Postecard because it reuses the backend from Pastecard, but, you know, it makes posts.1 Of course, there were a few hiccups along the way. For example, it literally uses the Pastecard backend, at the Pastecard domain, but I want it to display on my personal domain. So the actual tweet text is an iframe within the little tweet embed, to avoid cross-domain restrictions. Otherwise, the base functionality is the same: a little PHP script that takes my tweet input and writes it to a text file, and a little Javascript that reads the text from that text file when the page loads. There’s a little extra Javascript in that iframe to parse out and hyperlink URLs, making sure to load them in the parent frame. And, naturally, a little extra PHP to add the new tweet to the RSS feed file. I have a simple Shortcut saved as an “app” to both my phone and laptop that prompts for the new tweet text and sends it to the PHP script.

One thing to come out of the downfall of Twitter has been a resurgence in the mindset of owning or controlling your content online. A blog at your own domain name, a social media presence that you can decouple from the provider and take your follows, followers, and archives with you. This simple little project is a combination of both those things. I may not stick with it for as long as I have with these other things, but I do admit it feels pretty cool here at the start.

Update July 14, 2025

I’ve been posting tweets to my own little single-player Twitter for over a year and a half and really enjoyed it, with one exception. The front end lived here on my personal website and the back end (and feed) lived at Pastecard. So I started looking into a solution that would let me combine the HTML sandbox that is my personal website, with the freedom of a server that I could program and save data to, while still being completely free. And I found it in Cloudflare Workers.

The amount that you can do at Cloudflare for free, let alone the amount of internet stuff that Cloudflare does, period, is kinda mind-blowing. Their Pages product offering is a lot like Github Pages, where this website used to be hosted (spoiler!) — essentially unlimited static files hosted for free and distributed across a fast, global CDN. Their Workers product is managed infrastructure for web apps written in Javascript with a free tier that exceeds any usage I’d ever have, and the same for their KV data storage. I migrated my personal site as-was to their Pages product no sweat, and started to think about a standalone Worker that would handle the storage, display, and feed generation of my lil tweets.

Being my first attempt at a JAMstack project, every step forward required at least one Google–Stack Overflow round trip. So, begrudgingly, I created a Claude account. They advise being really comprehensive and verbose in your initial prompt if you’re going to stay on the free tier, so I laid out my whole plan to use Cloudflare Workers and KV, and all the functions I would need, and it basically nailed it on the first try. There were plenty of things I finessed to make it look the way I want, but it wrote endpoints to post (i.e. save to KV) a new tweet, bulk-post a CSV of all my previous tweets, return the newest one for my home page, return all of them for an archive page, and return RSS and JSON feeds of all of them for following purposes.

The biggest hangup was purely my fault. I thought I could leave the site in Pages, the now-working tweet functionality in Workers, and set up routes on the main site domain that pointed to the worker endpoints. There’s documentation and settings that imply this is still possible, but I couldn’t get it to work. So now the entire site is actually a Worker web app, with routing logic such that the tweet-related endpoints go to their JS functions, and everything else goes to one big folder representing the site as it’s always been. Everything outside of these tweets I can continue to play around with as static files, and never have to worry about how they interact with the functional code.

There were some things Claude did that needed to be corrected. One was a sequence of helper functions I had to turn plain quotation marks into smart ones, and hyperlink URLs. Claude implemented them in the opposite order, meaning <a href=""> turned into <a href=&ldquo;&rdquo;> and that was no good. I also spun my wheels a long time on timestamps. Since Cloudflare is distributed globally, calling Date.now() as Claude did kept giving me the Unix epoch, since otherwise you could get one time from one server and a different one from another. After working around that and then also dealing with accounting for daylight saving time, I shifted the responsibility for attaching a timestamp from the server side to the client side — which, by the way, is now a full iOS app that Claude helped me write too.

All of that to say, I now have my ideal setup for this little personal Twitter app. It is fully integrated into my personal website, as it should be. They’re one and the same. So the Postecard name doesn’t really apply anymore, sadly.

Update November 22, 2025

I stopped using Instagram in 2020, a few years before I stopped using Twitter. Ever since then, I’ve wanted another place to post the occasional photo. I briefly revived my old Flickr account, but usually just posted photos to Twitter. This solo Twitter project originally being based on Pastecard meant it was text only; I could post a link to a photo but the photo had to be uploaded somewhere else first. So on a whim I asked Claude if it was possible to upload an image file to the static file directory of my Cloudflare Worker-hosted website via a Worker function, and it said no. It was definitely possible, and actually easy, to instead write an endpoint that took in an image file, uploaded it to a Cloudflare R2 bucket, and then assigned it a URL so it looks like it’s hosted at the website too. And Claude generated that endpoint after a simple confirmation, and once again, it worked immediately.

All of the hard work was moving the computation into the iOS and Mac app that posts tweets. This app was kind of an afterthought introduced right before the last update, used to generate an accurate timestamp for tweets that it could pass along to the Worker as a simple integer. Well now, the app hooks into the device photo library, picks a photo, forces a square crop of said photo (more on this later), and converts it to a JPEG. It also does all the text processing (not just timestamps, but also smart quotes, hyperlinking, and HTML escaping) before passing the text content of the tweet. Claude did the bulk of the work on image processing, and I figured out the right order for all the functions to trigger, plus the necessary HTML and CSS for the final webpage layouts.

Each post can only have one image for simplicity’s sake. A post can have only text, only an image, or both. The image is forced to be a square, a little bit in homage to the original Instagram, and a lotta bit because it made laying out the tweets with inline images easier across different screen sizes. And while it was very easy to use a built-in photos API to have the app select a photo from the device’s library, it was so much work to build the screen that crops it to a square. There’s no API out of the box for this, everyone has to implement it themselves. And it’s math on math, to figure out the shorter of the two image dimensions, force that as the maximum cropped dimension, also not to let the user zoom out and essentially letterbox or pillarbox the end result. Then from there, to make it feel native, implement double-taps and pinch-to-zooms from scratch that scale the image within the crop. And finally the low-level data nonsense to turn the square on the screen into a fresh JPEG file of alphanumerics that travels safely via an HTTP request. Who knew!

I think this is as good as it gets. Sure, more established social media apps support multiple images per post, and really the present and future of the internet is video anyway. But I don’t want nor need a full-fledged social media replacement on my own website, just an easy way to post jokes, cool links, memories, and, every so often, a phone photo. And now I have a custom app on my phone and my laptop that does so, to my personal website, beyond the control of the big companies. (Except Cloudflare.)