The website you're currently on! Powered by my own static site generator and server.
I created this website in the summer of 2023 to have a somewhat-independent internet presence. The basic goal was to feature some of the projects I've worked on and potentially a blog, but my real motivation was the desire to put as many unnecessary features in it as possible.
While I haven't implemented very many of my ideas yet, I've done enough that it seems worthwhile to explain/track my progress here.
What kind of website has only one appearence? Obviously, dark/light mode is a must, but the dream is to have a wide variety of themes that users can pick from, each completly changing the style of site. Want pure HTML with no styling whatsoever? I've got you covered! Want a scrolljacking nightmare where each word flies around your screen? It's coming soon!
Everyone loves HTTPS. (Almost) any site can be securely and simply accessed using HTTPS over port 443. But it's so BORING. I want MORE. Only have port 22 available? Feel free to SSH on in! You'd rather use WHOIS? I'm still trying to think of a way to deliver content with it, but when I do, get ready!
I'm selfish. I don't want maintaining and updating this ridiculous monstrosity of a website to take any actual effort. So, while writing the site's generator/server for all the different versions was/is a pain, the actual content is stored in one place with basic markup, with edits instantly reloaded locally and easily pushed to production. How's that for Developer eXperience?
Given all the different formats I'm serving this content in, it was important from the beginning to have some sort of greatest-common-denominator structure containing not just the all content, but also some semantic information for presenting it well. Defining this structure allows me to know exactly what can be represented when writing content, as well as what a new presentation format must be able to represent.
Take, for instance, this page. So far, you've encountered three sections: the Overview, Main Features, and now the Content. While all three are sections containing mostly text, the "Main Features" section serves a completly different purpose, being a list of broad goals with short descriptions rather than some general content with a header. Creating a new type of section for this in the content structure allows all formats to present such lists effectively, without having to customize each page for each presentation format. Not only that, but elements like the title and subtitle also need to go somewhere, and defining a content structure upfront forces me to clearly define what's allowed and not (for example, a subtitle contains hypertext, including links, while titles are plain text).
While something like Markdown is the typical choice for static-site generators, making writing typical posts a breeze, it just wouldn't do for this use case. I needed room for encoding custom structure along with my content. So I next turned to YAML, a "human-friendly data serialization language" that could surely encode the structure I wanted. However, trying to write content in YAML just didn't feel right (despite their website's success in doing so). So, I finally settled on everyone's favorite data format...
XML. I don't think I've met anyone who claims to like or promote XML's usage, and I don't know of any modern project that's chosen to use it with no external pressure, but I really like it for this use case. Its separate tag names, attributes, and tag contents map nicely to types of content, extra non-displayed information, and actual on-screen content. Take the humble link; XML can represent it with a simple <a> tag, with href attribute for the destination and (in theory) any textual content you want between the tags. Sure, you can do the same in JSON, but you don't get that clear delineation between the destination and the visual content, it's all just attributes.
The XML describing the Main Features section above. Beautiful, isn't it? (At this time, I have yet to add code blocks, so we'll all have to make do with screenshots.)
So, with all my XML files for projects, posts, and other pages stored in a content directory, I serialize it into one big content struct with using serde and quick_xml (along with some basic directory walking, I'm not (yet) crazy enough to put everyone in one XML file). Then, each format implementation (aka presenter) gets a reference to the content struct on rebuild, from which it creates and serves its presentation.
With the content all deserialized from XML into a nice machine-readable struct for passing into various presenters, the problem now becomes keeping it up to date with changes. Initially, I just loaded the content once on startup, and used cargo watch for updates on edits. Obviously, given Rust's famously-slow compile times, this just wouldn't do for actually writing content.
So, I switched to a decent (if unidiomatic) solution of having a global static RwLock<Content> variable that could be updated on any change, at which point a message-passing channel informs all running presenters that they should reload their content from the global content struct.
However, for most forms of content, there is another form of reload that needs to happen when format-specific content (such as HTML templates) changes. For presenters like SSH, these behave the exact same way (just update state for new connections), but for others (HTML), they are handled separately. In HTML's case, updates on content changes take only about 80ms locally (and only 5ms compiled in release mode), while updates to templates take 200ms (and 10 whole milliseconds in release). Obviously, this falls completely in the realm of premature optimization, but why rebuild templates unnecessarily? (Also I was bored on a 5-hour flight).
Of course, even if content reloads instantly server-side, refreshing the page on every tiny adjustment to template styling gets annoying really quickly. So, I added a feature to inject a small auto-refresh script into every HTML page when enabled. While polling for content updates is probably the best way to implement this (simple to implement, works through server restarts), I decided to use WebSockets because I'd never used them before. So, after content changes, a message is sent to all active websocket connections telling clients to reload, making new changes instantly visible and enabling easier bikeshedding background colors to procrastinate actual work.
The HTML version of this site was the first presenter I created, and currently includes 3 Themes: Default, Simple, and Pure. Each of these is really its own sub-presenter, but they're grouped under one object that handles serving the correct theme to users.
In terms of presentation, the pages for each theme are currently rendered using the Tera templating engine. For the Default theme, CSS is automatically generated using railwind, a Tailwind CSS clone written in Rust for easy integration into the site generator. For the Simple theme, the CSS is stored alongside the templates for easy editing, and the Pure theme has no CSS to worry about at all!
The main component of the HTML presenter functionality-wise is allowing the user to switch their site theme. I initially planned on hosting each theme's content at a different path, making this a true static site, but ultimately decided to use a cookie to store the user's preferred theme and serve the correct site based on that. In the hypothetical event that someone links to this page and the person who clicks the link has visited before, this has the advantage of saving their desired theme.
To change themes, a query string parameter called "version" can be passed into any page, which will update your local cookie and serve the requested theme (or the default, if none exists). This is most easily done on the Change Theme page, which provides links for easy switching.
The SSH presenter was the second one I worked on, but was the idea that inspired me to make the site to begin with, motivating the extensible design that allowed for the multi-theme HTML presenter I built first. It works using a custom server built with the russh library, which implements the tricky parts of SSH such as authentication, key exchange, and encryption. The server (for now) just handles simple session with one channel, with the majority of my effort going into handling incoming data rather than in intricacies of the SSH protocol.
Content for the SSH presenter is stored in the form of a virtual filesystem that the user can navigate. When content is rebuilt, a directory is created for projects and a .txt Markdown-like file is created for each page. The user can then explore this filesystem through a shell-like interface.
When a client connects, the server creates a new SshSession struct containing a reference to the SshContent (the virtual filesystem), a shell struct (handling line discipline and other shell stuff), some info about the user (username and terminal size), and potentially a currently-running app (such as vim).
If there is no currently-running app, incoming data (keystrokes) are sent to the shell struct, which echos back must characters and handles control characters like backspace appropriately. It also stores the user's history to allow for moving up and down through past commands. If the user runs a command by pressing enter, the shell returns the command text for the session to handle. For most commands, like ls, cd, and cat, the session can handle them immediately, appending their output to the data to send back to the user.
However, other commands, such as vim (currently only vim, I'm open to other suggestions), can't just output their data and be done. These are full-fledged "apps" that must be stored in a session while running to have incoming data routed to them instead of the shell. In vim's case, this data is used to support basic cursor movement and correctly-scrolled rendering of the document (a couple hundred lines of surprisingly-tricky code despite its simplicity). Once the app is closed, the running app member of the SshSession struct signals that it is done, and control returns to the shell.
Overall, the SSH presenter seems to work pretty well, despite all the complexity I ignored when implementing it in favor of actually finishing it. The main downside of it so far is the ridiculous number of SSH bots roaming the internet, logging in every few seconds to attempt an exploit using features my server is nowhere near supporting. But the timeouts deal with them pretty well, and I can only hope I've ruined a few people's days who think they've successfully broken into something only to see my welcome message and virtual filesystem waiting for them. Maybe I make this work to my benefit somehow — if you're looking to promote a better SSH brute-forcing bot, get in touch for some highly-targetted advertising!
When I described the SSH functionality of this site to my dad, his first response was that it sounded like Gopher. I had never heard of Gopher before, but it is a 1990's protocol for organizing and distributing documents over the web. Wikipedia claims that it "fell into disfavor, yielding to HTTP," but I still think it's a pretty cool to organize information, especially for simple documents (no web apps on Gopher!). Plus, it works well in a terminal, and is a pretty simple protocol, making it a natural protocol for me to implement.
So, I got to work serving content over it, following this blog post about it. In hindsight, using a library for this was overkill, and doesn't fit very well with the way I handled SSH and HTTP (render content to presenter-specific format, then display logic is simple). It did make it easy to implement in just an hour or two, so feel free to try it out! (I use Bombadillo as my Gopher client).
The basic structure of the Gopher site is pretty simple, roughly following the HTTPS site in structure, with a Gopher menu for each page, with markdown (non-canonical info lines) for content and submenu links for each internal link. I don't know that using menus for content pages is in the spirit of the Gopher protocol, but it's otherwise impossible to put links in content, which is something websites like this do a lot of. I do link a plaintext version of each page at the top as well if you prefer pure documents to menus containing links. It seems to work pretty well, apart from images, which rarely seem to download successfully.
Everyone loves the Quote of the Day protocol (RFC 864, one of the shortest RFCs I've read). After all, who wouldn't want a reserved port for when you need a random (not necessarily or even usually daily) quote or message? I certainly do!
To satisfy all your quote needs, just connect to this server (or another, I guess) on port 17 over TCP (I just use netcat), and you'll get a random quote from one of these project pages. To generate quotes, I just grab all period-delimited sentence from the markdown generated for SSH. Often, this ends up including (or just being!) part of a link, but to me, that just makes the quotes even better. Enjoy!
Note: UDP is not supported mostly out of laziness, with the excuse being fear of amplified DDoS attacks via my QOTD service (we send up to 512 bytes in response to even a 1-byte input! Such amplification! Just too dangerous for the modern web).
Add a list element to the system so I can properly render this list
Add a quick-switch theme to the HTML presenter to allow easier switching between themes while on a page (mostly for previewing my own changes)
Fix Gopher protocol image downloads, and possibly rewrite the whole thing (it's pretty short, don't worry)
Update the SSH vim app to be harder to quit (add command-line editing)
Add basic tab-completion to the SSH presenter's shell