Weird Web October 2024: A Retrospective

A tie-dye background with the words
See my weird websites here.

I meant to write this retrospective a couple of weeks ago, but between the US election and planning a cross-country move, I’ve been a bit busy. Still, never late than never!

Weird Web October (WWO) was an Inktober-style challenge put on by Jay Zuerndorfer. Participants were invited to make a website each day, based on a prompt.

I only found out about it on the 10th of October (from @latte’s Mastodon), so that’s when I started. You can see all my entries here.

In total, I completed 11 of the remaining 21 entries! I would’ve liked to have done more, but I got a nasty cold half-way through while travelling, and I never quite got back my momentum after that.

In this retro, I’m going to talk about my feelings on the challenge as a whole, and then talk about each entry that I completed.


A Website A Day Keeps the Boredom At Bay

All in all, I really enjoyed this challenge!

One of my biggest creative hurdles is finishing projects. It’s dispiriting how many half-baked projects I’ve abandoned over the years.

WWO forced me to get each website out in a day, no matter how janky or unpolished it ended up being. The constraint really helped me keep my ideas in scope. I might come up with some cool idea for a prompt, but if I didn’t think I could do it in a day, it was a non-starter.

While that could’ve led to boring, unambitious projects, I pushed myself with some of them, and am proud of what I accomplished!

I’m currently on sabattical, and I don’t think I could’ve finsihed nearly so many websites if I was working full time. When I was working as a programmer, I rarely had the mental energy left at the end of the day to work on my own projects. With that in mind, I’m really impressed with all those participants that made their websites while working. Y’all rock!


The WWO website used a web ring technology called Octothorpes to publish everybody’s entries on a public page.

All you had to do was add a little snippet of HTML to your site, visit it, and it would show up on the master list!

For example, here’s the list for prompt #10:

A screenshot of the #Art page, with a collection of links leading to other people's entries.
https://octothorp.es/~/Art

I really enjoyed looking through everybody’s entries each day and definitely took lots of inspiration from them.

Some of my favorites were:

  • ribo.zone’s text adventure entry for #Nature
  • anhvn’s SVG theme changer entry for #Color
  • weird.sha.codes CursedTube for #Cursed
  • jzplusplus’s #API entry which just redirects to a random other WWO website
  • Pretty much everything put out by Marie Malarme. Incredible!

Big shoutout to weird.sha.codes and Marie Malarme for being, I think, the only people to complete all 31 entries!

General Tech Thoughts

Naturally, I used the most hacky and fast code I could to make my ideas come to live. Like I mentioned above though, that really helped me push past my unhelpful perfectionism! But, uh, don’t use any of this code without checking it first, bugs abound!

I learned a lot making these sites. Despite working in Web tech for ten years, my mental model of CSS still feels bad, and this helped improve it a bit!

I also learned a bunch of new-to-me CSS features (:has, :target) and a few new JS APIs (MediaDevices).

I did my best to make every website work across different viewports, although some, like the keyboard only game for #Dimensions or the phone only experience in #IRL didn’t meet that goal.

I definitely skimped on some accessibility stuff – almost none of my images had alt text, and I’m sure keyboard navigation was broken on some sites. That said, most were so simple that I (hope) they work ok with a screen reader.

Overall, this was a way to flex my programming muscles, learn some new things, and ignite a passion for web dev again.

Now on to a breakdown of my entries :)


My Entries

#10 Art

A screenshot of my #Art entry. Shows a gif of a dancing stick figure, with a grim-reaper that bobs up and down to the music. There's a button below them labeled 'stop' which stops the Death animation.

This was a fun recreation of my favorite Emily Dickinson poem: Because I could not stop for death.

I cheated on this one, because I’d already recorded the song and drawn the pixel art grim reaper animation before I’d learned about WWO.

But! WWO gave me the motivation to fully bring this idea to life. See above about finishing projects…

Tech Notes

Don’t have much to say on this technically, other than it was a PITA to splice together my poem audio with the Devil Went Down To Georgia mp3. I figured it would be easy with built-in MacOS audio tools, but apparently not.


#11 API

A screenshot of my #API entry. Shows some text, along with a JSON blob containing request information. The website is pretty text-based, visit it to get a better idea :)

This one was much more of a technical challenge! In it I report back all the information that my server gathered when you visit the site, including IP address, a map of where you are, and cookie information.

My intent was to show people the information that a website can collect on you, just by visiting it. Like, do you know that your IP address can be used to find your general real-life location? If your IP can be associated with your name somehow, a malicious web server can report where you are. Spooky!

I’d like to expand this website into a more detailed op-sec post someday… I’ll link it here if I ever do!

Tech Notes

This site actually marks the first path on ragman.net to be served non-statically. My server is written in Go, and I had to make a quick little function to inject the request information into the served HTML.

I spent way too much time trying to get the JSON to be syntax highlighted. Ended up just calling pandoc which is what I use for the code snippets on this blog.

I also used Leaflet JS for the first time, to display the map of the visitor’s location. I was happy with how easy it was to use.


#12 Ritual

A screenshot of my #Ritual entry. The header reads WWO 12 Ritual, with an image of a mug of tea. Text below the image reads 'Hi there! Here's how to make my morning tea!'. A link below that reads 'Begin'.

I’m quite fond of this one. And now I’ll always have something to show people when they ask me about tea!

It’s fun to make simple projects like this – just sharing something you love with the world.

Tech Notes

I was inspired by ribo.zone’s text adventure entry when I was creating this site.

They use a cool CSS-only technique to display different <section>s when the visitor clicks a link.

Each <section> is the same size, absolutely positioned, and has a unique id. Each <section> initially has a z-index of auto (0), meaning they’re all placed on top of each other, in the order that they’re laid out in the HTML.

Each <a> link on the page contains an anchor to one of the <section> ids, e.g. https://ribo.zone/weirdweb/nature/#look.

They then use the CSS :target pseudo-class to selectively give whichever <section> is currently active a higher z-index of 2, which causes it to be displayed above the rest.

And v’oila! Javascript-free text adventure!

Unfortunately, this technique only works when each <section> is the same size. If the section with z-index 2 was smaller than a section below it, the other section would be visible behind it.

I wanted to use images of different sizes, so I had to come up with a way to solve this. Fortunately, with some googling around, I was able to come up with:

section {
    display: none;
}

section:has(~ section:target) {
    display: none;
}

section:first-child,
section:target {
    display: flex;
}

The section:has(~ section:target) selector is required to hide the first-child section when another section is the current target.

It can be translated into English as:

Select all <section> that have a subsequent sibling <section> which itself has an id that matches the current URL fragment.

I could’ve avoided it if the initial URL always had an anchor fragment (e.g. https://www.ragman.net/wwo24/ritual/#morning_tea), but I wanted to make sure the normal /wwo24/ritual/ path worked.

I’m thinking of redesigning my home-page to be text-adventure-y, and I might use this technique to do so!

One final note is that this is the entry where I used this footer format, which I stole from Mitchell Busby’s #blessed entry.

Others by me | All websites for #ritual


#13 GIFs

A perfect loop gif of the character Fio from Porco Rosso moving a plane's wings up and down.

Perfect Loop GIFs! I was briefly into perfect loop gifs in, like, 2015, and had a small collection of them on my computer.

I remembered some blog post I’d read back then talking about a tool that the author had made to automatically pull them out of videos.

I’m not sure if this post was the one I’d read back then, but I found it while Googling, and decided to try to run Python the code provided!

I’m happy with some of the ones I’d found, especially the Porco Rosso one above.

And here’s one I found later on Halloween when I ran Scooby Doo and the Witch’s Ghost through it. It doesn’t hold up nearly as much as the Zombie Island one, but it was a fun movie to watch on the spookiest day of the year! :D

A perfect loop gif of the character Thorn from Scooby Doo and the Witch's Ghost, waggling her fingers like she's casting a spell.
🎶🦇 I’m a Hex Girl! And I’m gonna put a spell on you! ⛥🎶

Tech Notes

It took me some tweaking to get the coding working, since the moviepy library had changed a bit, but I eventually got some code running! Unfortunately, I updated pip on my machine sometime after doing this entry, and now my code is broken, and I don’t want to go back and fix it. Maybe one day!

I’m trying to get into the habit of downloading youtube videos (using yt-dlp) that I want to watch, rather than watching them on YouTube itself, where I inevitably get sucked into watching more and more videos.

This meant that I had a collection of videos already on my computer, which I fed into my program. It took ages to run, and the algorithm wasn’t that precise, so there were a bunch of false positives.

I’d like to improve the algorithm, or see if somebody has better code out there, because I really like these gifs!


#14 History

A screenshot of my #History entry, showing a message telling people to check their browser history.

I had originally wanted this history to be about, ya know, actual history. I had just finished listening to the Cool People Who Did Cool Stuff episode about the “Kabouters in late 60s Amsterdam who rebuilt the city”.

I think I wanted to do something with their planting of the orange tree by the Amsterdam National Monument. But I decided I didn’t have a clear idea, so I pivoted, and decided to focus on browser history.

Give the website a try, and you’ll see that if you click the links you’ll eventually get multiple browser entries that together read: “Remember to drink responsibly”.

I think the browser history hacking was a cool idea, but not one that I executed very well. I had multiple friends not actaully check their browser history, and having the final link still look like a link was confusing. Things to improve on!

Tech Notes

I figured the only way that a website has to interact with a browser’s history is through the document title. I did some testing to see if it updated if you changed the document title with javascript, and lo and behold it did!

I used URL fragments to provide a unique entry in browser history for each clicking of the link, and then changed the document title using Javascript.

You can see the changed title in a desktop browser tab, though not really on mobile.

A screenshot of my browser window, showing that the tab has the title 'Remember'.

Most people have so many tabs open on their browser though that it won’t give it away :P


#17 Dimensions

A screenshot of my #Dimensions entry, showing my Super Mario 1D World Game! There's a line of bricks, with Mario on the left, and a Goomba advancing from the right.

This is maybe the entry I’m most proud of. If you haven’t played it yet, please do, it only takes about ten seconds :D

Somehow, even though I know the joke is coming, it still cracks me up everytime.

I’d originally thought of doing some homage to Flatland by Edwin Abbot, but I gave it a skim, and boy sexist and classicst than I remembered. I mean, that’s the entire point of the book, to criticize the society at the time, but it wasn’t what I was in the mood to read.

Fortunately, this Mario idea popped in my head! We have Super Mario 3D World – why not 1D world?

Some sprite assets, music, and hacky javascript code later and the game was born!

Tech Notes

Don’t have too much to say about this one. I messed around with sprite importing, which was fun, and wrote some really hacky canvas code to animate them.

Feel free to look at the code, but I wouldn’t recommend copying anything from it!

One effect I really liked was Mario getting stuck to the Goomba and being pushed off. In the normal Mario games, he would fall off the bottom of the screen when he dies, but this is 1D baby!


#18 Words

A screenshot of my Weird Web October page for prompt 18, Words. The words 'Picture Frame' are used to make a rectangle. Inside the rectanglular frame, a link with the word Moo Deng is centered. Clicking on the link generates the name of a different piece of art.

A riff on the expression “A picture is worth a thousand words”. Cuz like, sure, you need a thousand words to describe a painting.

But you can evoke a whole image with just a few words:

Disco ball

Or even a whole emotional response:

Your first crush

I really like how this one turned out, though it didn’t get the reception I wanted from my friends 😅.

Tech Notes

This one was pretty simple, the only tricky thing was swapping out the word in place. The correct amount of padding had to be added to both sides to make it centered.

function getPaintingHTML(painting) {
    // const frameLength = paintingEl.innerText.length
    // In other words, "P I C T U R E F R A M E P I C T U R E".length;
    const numPadding = (frameLength - 4 - painting.length) / 2;

    const paintingLink = `<a id="painting_link" href="javascript;">${painting}</a>`;

    return `R ${' '.repeat(Math.floor(numPadding))}${paintingLink}${' '.repeat(Math.ceil(numPadding))} E`;
}

#21 Memes

A screenshot of my Meme Radio page! Shows the Leek Spin meme with two buttons titled 'Turn on music' and 'Next meme'.

Meme music! Millenials’ contribution to the annals of history!

This one was a lot of fun. I ended up going with five meme songs:

  1. Leekspin
  2. Nyan Cat
  3. Skibidi Toilet
  4. Peanut Butter Jelly Time
  5. Rickroll

If you’re reading this and want me to add one, let me know :P

I mostly showed these sites to friends or family that are my age and older, so not all of these landed. Skibidi Toilet was off-putting to the older generations, and I was surprised at how few people knew the Leekspin meme.

The headbanging cat was a universal hit though.

Tech Notes

This one was pretty straightforward, but I did run into an issue with Skibidi Toilet audio synchronization.

It was the only meme that had a strict start point, in contrast with, say, the endlessly animating Nyan Cat, so its music had to be synced up.

The code refetches the gif when you transition to Skibidi, but if you click the “Turn on music” button in the middle of the gif, it’ll be out of sync.

I ran into another problem with the gif – it was so large that it wouldn’t be loaded by the time the music had started. In fact, I was originally hitting server write timeouts, which were causing the GETs to 503.

I upped the timeout, which fixed that, but on slower internet connections the download was simply too laggy to sync up.

One way to fix this might be to wait until the image is completely loaded before playing the audio… but hey, it was made in a day! :P

Ideally I would also use a <video> tag instead of gifs, which would solve the audio sync issues, and be much more bandwidth friendly. But I couldn’t figure out how to get the background-repeat effect with a video tag, so I gave up.


#22 Bugs

An image of a Lanternfly insect. It has brown outer wings with black polka dots, black and white wings in the middle, and then a final set of red wings. It's beautiful!

I don’t really have anything else to say about this one that isn’t said in the post: Go read it!

Only thing is that the background picture is of the Wissahickon Park in Philadelphia, one of the largest urban parks in the country. If you’re ever in the area, I recommend visiting!


#22 IRL

A screenshot of my #IRL entry. Shows a picture of table with a purple toy alligator overlayed on top of it.

To my eternal shame, I said called the character a digital crocodile when he’s really an alligator. I even got him in New Orleans! 🙈

I had a good time with this one. Made me think about how (relatively) easy it is to create a little interactive story. I want to do more things like this in the future.

I recorded the audio in my bedroom, took a photo of the Digital Crocodile Alligator, and then used Aseprite to remove the background (I really should learn how to use Photoshop one day).

When sharing this with friends, many didn’t read the message to turn their audio on, and so didn’t understand what the point was. In the future, I’d make that the only warning message, to hopefully get people to actually read it!

Tech Notes

This was my first time using the MediaDevices API, to get the camera stream.

It turned out to be very easy! All the code to get the stream and put it in a <video> tag, and play the audio once it had loaded is below:

const constraints = {
    video: {
        facingMode: {
            exact: 'environment'
        }
    }
};

try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    videoEl.srcObject = stream;
} catch (e) {
    videoEl.style.display = 'none';
    splashEl.innerHTML = "<p>Could not get rear-facing video stream.</p><p>If on a phone, check permissions, refresh, and try again.</p>";
    splashEl.style.display = 'flex';
}

videoEl.addEventListener('loadeddata', () => {
    audioEl.addEventListener('playing', () => {
        crocEl.style.opacity = 1;
    });
    audioEl.addEventListener('ended', () => {
        crocEl.style.opacity = 0;
    });
    setTimeout(() => audioEl.play(), 1000);
}, { once: true });

Great job to the API developers!

I did have a little bit of trouble getting it to work on IOS – it was popping the video into some full screen thing, which then made it so the crocodiile didn’t show.

The fix was adding the following attributes to the video tag:

<video autoplay playsinline></video>

Thanks to A for the help debugging!


#30 Emoji

A screenshot of my #Emoji entry. Shows two text boxes, one for english and for emojilish.

I took a week long break before after the IRL entry, due to sickness and travel. Came back strong with this one though!

The emoji translator was inspired by my friend and former co-worker G$, who would write like this all the time.

I sent this website to him, and he said that he’d been thinking of making something similar for a while – I’m happy it met his approval :D

I had always planned on adding the Smash Mouth example, but I added the Hobbit example because I wanted something more “literary”. Then I added the Monkey King snippet because I was reading it at the time. Definitely took some liberties with “North”, “South”, etc.

Tech Notes

The translator is pretty simple, it just takes every word of text and looks for the largest substring inside that corresponds to an emoji.

For example, the word “told” gets turned into “t-👴” (t-old). Looking for the largest substring is important, because otherwise short emojis would be everywhere, and longer ones would never be shown, e.g. “told” would be “2️⃣ld”.

The key to the translation is the word -> emoji mapping, which currently is something 623 entries. I created it by scraping https://unicode.org/emoji/charts/full-emoji-list.html and filtering for any emoji whose CLDR Short Name was only one word long.

So it got robot -> 🤖, but doesn’t have an entry for alien -> 👾 because the short name is actually “alien monster”.

If I’d had more time I would’ve gone through and added a bunch more emojis from the longer CLDR names, which would make the translation much more impressive.

When I shared this with a friend, he said that somebody at his work had made a Slack bot that did this, but using Levenshtein distance to convert every word to the emoji it was closest to.

I gave this a try, and found the results pretty incomprehensible. Maybe it would work for short strings of text, but meaning isn’t preserved across multiple sentences.

Right now it only translates one way – it would be fairly simple to go the other direction, so that’ll be future work for sure.

I might also make a browser extension that does this, just for funsies 😄.


And that’s a wrap! I had a blast making these sites. It really sparked my creativity – I want to find a way to keep doing similar throughout the year.

If you read through all this, thank you! You rock! And I hope you go out there and make something creative yourself! ❤️