It was around April 2022 that I'd made my original website, and if you can't tell, I knew very little about programming back then! Fortunately, I like to think I know a little more now, and after being inspired by elle(who has a really nifty site, consider checking it out), I decided I'd start from scratch on a brand new site, one with a little more substance. I had two goals in mind for this new-and-improved site: I wanted to add a blog with an RSS feed, and I wanted a comment section for each blog post.
Picking a framework
The first decision I had to make was how I was going to create the website. Plain HTML/CSS/JS is great, but I'm a sucker for tools that allow you to generate things like layouts dynamically. I also wanted the website to be (at least almost) entirely functional without client-side JavaScript, so a framework that supports server-side rendering is important. After some cursory searching online, I first decided I'd give Django a shot, since I've used Python a fair bit in the past for personal projects.
It wasn't for me! I'm sure if I gave it more of a chance, I could've gotten the hang of it, but I ultimately decided I'd switch gears and use Svelte instead. Technically, I'm actually using SvelteKit: Svelte lets you create interface components for websites, whereas SvelteKit has additional functionalities like handling page rendering. There were a couple reasons for choosing this over Django: First, the Svelte documentation is incredibly well-done; It's organized nicely, and they have plenty of tutorials that really help you internalize how parts of Svelte work in practice. The other(maybe less consequential) reason is that DailyTxT, an application I use every day, uses Svelte for its frontend!
Creating the development environment
Since I'd recently started using NixOS for my computer, I wanted to do things the "Nix way", which meant creating a development environment for my website using the Nix programming language. I'd done it before for some other projects, but this was a bit different, because I also wanted to have it accessible as a Nix package. I took some inspiration from Marius Niveri's flake.nix in order to create my own Nix flake for this project. I used Bun to set up the environment and handle package management, and I also set up bun2nix to convert bun.lock files into a Nix equivalent, bun.nix.
I figured out that I could set up my project with bunx sv create my-app, and I was greeted with a whole lot of files that I had no clue how to use! I ended up creating a list of the purpose of each file(even the ones I already knew the purpose of), just so I could keep my head straight:
.gitignore- List of files/folders that shouldn't be published to Git.npmrc- Config file for NPM, see https://docs.npmjs.com/cli/configuring-npm/npmrcbun.lock- Locks installed Bun packages to the specified versionsbun.nix- Generated bybun2nix, makes it possible to build the project as a Nix packageflake.lock- Locks installed NixOS packages to the specified versionflake.nix- Sets up the development environment, generates the Nix package(s) for the projectpackage.json- Used byflake.nixto determine package and script informationtsconfig.json- Helps IDEs determine how to treat different files in the project(for purposes like autocomplete)svelte.config.js- Configuration for Svelte and SvelteKitvite.config.js- Configuration for Vite, which Svelte uses under the hood.env- Node.js environment variables, see https://nodejs.org/api/cli.html#environment-variables-1
Due to some of the recent controversy involving Bun and its Anthropic acquisition, I later decided to switch to using Node.js instead, which is actually better-supported by the NixOS ecosystem anyways. That saw the removal of bun2nix and bun.nix, and the replacement of bun.lock with package-lock.json(though they're functionally the same).
During setup, I was prompted to use either TypeScript or JavaScript, which was a hard decision; I liked the idea of being able to specify types with TypeScript, but I'd never really used it before. I started out with JavaScript, and I still use it for some of the inline scripts, but tried out TypeScript for a lot of the server-side scripts, which was fun! I don't know that it's so necessary to verify types when I'm the only one sending input to the scripts, but I figure it's a good learning experience either way.
Acquainting myself
I then got to the actual site-making! I followed a lot of the examples in the Svelte documentation as I played around with this new framework, but eventually I turned my attention to one problem in particular: meta tags. Before I get into that, let me explain SvelteKit's "layouts" feature. See, SvelteKit lets you specify a "root layout", which is a set of scripts and HTML that every other page uses as a basis; for example, as I write this, this website has a header and footer on every page, which is specified in my root layout. You can also have layouts at other locations in the app, but they'll still inherit the elements of their parent layouts.
What I wanted to do was have a script in my root layout that would define the meta tags that each page would have. For the blog part of my website, I would have a different set of default tags(e.g. setting the OpenGraph type of blog posts to "article"). I also wanted to be able to be able to override tags for specific pages, just in the event of some edge case. I actually managed to get pretty far implementing my own solution, but I encountered an issue: Svelte doesn't support deep merging! Deep merging, also called recursive merging, is when you merge two data stuctures without discarding data from nested lists(for a more concrete example, refer to this GitHub issue for SvelteKit).
I could've implemented it myself, maybe, but I didn't want to have to maintain something like that, and I figured there must be some solution out there that could handle it more effectively than I could. I held out on making this decision for a long time, because I didn't want to add too many external dependencies, but ultimately decided to use svelte-meta-tags to handle my SEO. For what it's worth, it works quite well! My root layout defines specific meta tags, and my blog's pages add and overwrite some of them.
Randomly tiled backgrounds!
There was one idea I was really committed to: a background for the website that changes randomly every time you reload the page. My idea was that I'd have a folder containing some image files, and those would be randomly placed into tiles which would become the website background. This one stumped me for quite some time; you see, although the CSS background-image property supports multiple images, they don't automatically get placed next to each other. Instead, they stack! You'd have to predefine the offset of each image, which would require knowing in advance the dimensions of each image. Even if you did that, the background-repeat property doesn't play well with that sort of behavior.
I was determined to find a better way; surely, there must be some way to tile the background programmatically. That was when I noticed something: the CSS background-image property supports SVG images! Jackpot! You see, I'd done some work in the past to generate SVGs from album covers, and I realized that I could use a very similar technique here. Here's how it works:
- The server fetches images from a predefined folder
- The server shuffles the images into a random order
- The server removes images from the array until the length of the array is a perfect square(e.g. 1, 4, 9, 16...)
- The server places the images into an SVG as
<image>elements
Step 4 requires just a little bit of math! You need three parameters to mathematically determine where to place an object in a grid: the size, the dimension, and the index. The size is the length/width(in pixels) that the entire SVG object should be. The dimension is the length of the array, which should be a perfect square(and is, thanks to step 3). The index is the index of each object in the array. You can find the x and y position of each object using the following formulas:
- x:
Math.floor(index / dimension) * (size / dimension) - y:
(index % dimension) * (size / dimension)
For example, let's say that size is 1024, dimension is 4, and our object's index is 2(which would be the third object, since JavaScript is zero-indexed). For the x coordinate, would get Math.floor(2 / 4) * (1024 / 4), which is 0. That means our object is 0 pixels from the left side of the SVG! For the y coordinate, you would get (2 % 4) * (1024 / 4), which is 512. That means our object is 512 pixels from the top side of the SVG. In a 2x2 grid, that would be the bottom-left object.
I later added the ability to scale the size of images in their grid: instead of taking up 100% of the tile space, you can set it so that each image takes up, say, 80% of their tile size! Some extra math was required to actually center each image in their tile, but it works pretty well. If you want to see the actual implementation, check my site's code.
TL;DR: every time you load the page, it fetches an .svg file, converts it into a data URI, and sets that as the page's background. There are a few ways this implementation could be improved, like supporting non-square arrangements, or being able to pick from multiple sets of images, but it works for my purposes for the time being.
Making the site
Once I'd finished the background, I figured it was time to make the actual site! I checked the source code of some of my favorite bloggers(in no particular order), like Terence Eden, Ruben Schade, elle, Alisa Sireneva, Baldur Bjarnason, and beeps.website to get a feel for the best practices when it comes to HTML and CSS tags. I know it sounds silly, but I didn't even know that a lot of these elements existed! You're telling me that there's a dedicated <article> tag? I must have just been using the wrong resources in the past, or something, because this semantic HTML stuff is awesome! This MDN page, I found, was especially helpful.
Adding themes
In any case, I'd created the bones of my site, and then I decided that I'd add some toggles to my site: one for a dark theme, and one to change the site's font to my handwriting. I'd created a font of my handwriting somewhat recently; I was partially inspired by this post from Chris Smith, and also by the idea that I could render my aforementioned DailyTxT journal in my handwriting. Like Chris Smith, I used Calligraphr to convert my handwriting into a font!
I consulted the Svelte Discord for recommendations on how to implement a theme on my site. Remember, one of my goals was to have the site be functional without client-side JavaScript; in order to support users without JavaScript, it's necessary to have a value that the server can access: that means using cookies instead of the browser's local storage. One annoying thing about Svelte(this issue I also encountered with the background image) is that there's no support for dynamically modifying or injecting variables into CSS. What I ended up doing was this:
- Upon page load, the server hook gets the value of the cookies(or uses a default value), and edits the
<html>element to add these as attributes. - CSS rules set the page's color-scheme based off of what attributes are set in the
<html>element. - When you press a toggle switch, the browser sends a
POSTrequest to the server, and in response the server sets or changes the cookie for the given value.
I'd rather not just throw values into the root <html> element, but I don't see a better method than what I'm using right now, so this is what I'm stuck with! It's not the most elegant solution, but it works.
Creating the blog
After setting up the themes, I needed to actually put content on the pages, since at the moment every page just had a placeholder. I wasn't quite sure how to go about this; all I knew was that I really, really didn't want to write the content of my site with plain HTML! HTML is forgiving, which is nice, but also means that it's easy to make small mistakes that you never catch. I'd rather write closer to plaintext than to a structured language, so I decided to set up MDsveX. MDsveX is a Markdown formatter for Svelte that also supports rendering custom Svelte components. The former bit matters more to me than that latter bit, but it's still nice to have, I suppose.
I wanted to, at the very least, have the blog part of the site use Markdown, but I ultimately decided I may as well use Markdown for most of my existing pages, since they're mostly just text. After installing it, and enabling it as a preprocessor in svelte.config.js, I wrote some code partially based off of Marius Niveri's homepage, and partially based off of Josh Collinsworth's blog starter, to import post data from a .svx file(a Svelte markdown file) into a +page.svelte(+page.svelte files are what's rendered at a specific route; if you have /licenses/+page.svelte, then going to example.com/licenses would load that file's contents).
Using the same sources of inspiration, I took a more programmatic approach for the blog posts; I created a function that would fetch every blog post in a folder, and return its metadata(like title, date, and slug). I used that to create a page that would show all available blog posts, and with some fiddling, added pagination; 10 blog posts show up per page, and a "Next" and "Previous" button are enabled when relevant. I later added a "Random" button that fetches a random post as well!
I later improved the post navigation behavior; previously, when you went to a blog post's URL, it would just load the file with the same name as the slug. This meant that you could neither set a custom slug, nor have posts with the same slug on different dates. Instead, I created a function with a map of all of the blog posts. By querying a certain slug, the function would get all posts with that slug(and as a bonus, it now supported files defining slugs separate from their filenames!). Then, the function would use the year, month, and day parameters to fetch the correct post(or the closest matching post).
Dates are hard
The hardest part of this whole system was formatting the dates. I wanted to have the year, month, and day in the URL, but then I also wanted dates to be optional, and I wanted to be able to specify hours and minutes and timezones! I ended up creating a function that would return UTC strings for things like the URL, and then you could also set it to use local times for things like showing the date a post was created on a page. If the user has JavaScript enabled, those dates will change to their browser's time, but I also set the TZ='Etc/UTC' environment variable in .env, so that the server's default timezone is UTC.
RSS
While I was working on all this, I was also working on my blog's RSS(technically Atom) feed. I wanted to style the feed, so that it looked nice even without a feed reader, but since XSLT is being deprecated, I needed to style it with CSS instead. RSS/Atom exists as XML, not HTML, and Svelte isn't built for XML. So, I created an API route, which means that I have to use TypeScript instead of Svelte syntax(this approach has its advantages and disadvantages). I modified the function that gets the blog posts, and added an extra parameter which returns the contents of the post rendered as HTML for the feed. The only issue is that if a blog post has Svelte components that rely on the page data, those will fail, because those aren't passed to API routes; I don't expect this will be much of a problem. I then made some adjustments to the feed(mostly thanks to Kevin Cox's post on RSS feed best practices), like adding pagination, and adding a special option to view all posts if you go to page -1(I then added this to the normal blog pagination as well).
88x31
Once that was over with, I wanted to add some 88x31 buttons! I set up a simple little page that would pull all of the images from a directory and put them onto the page. I wanted to be able to dynamically link to a button's URL if applicable, and have no URL if not, so I leveraged the filenames to do so. Any image starting with the @ symbol will be treated as a URL, and double underscores(__) will be replaced with periods. For example, the file @ocelot__lol.gif becomes the url ocelot.lol! If it's not a URL, it'll replace double hyphens(--) with spaces for the alt text, so that hypnotic--ocelot will become hypnotic ocelot. I also took some inspiration from beeps.website, and had the buttons scale up when you hover over them.
Compatibility
My site was working, and looked as intended in my browser of choice(LibreWolf), but what about other browsers? I spun up my development server, opened up a port on my laptop, and checked out my site on other devices of mine! With Firefox on iOS, I noticed that WebKit had some default styles that messed with the way the site was supposed to look, so I wrote some extra CSS rules to account for it. I checked the site out on Ungoogled Chromium, the text-only browser Lynx, and even tested the site out on the 3DS(partly inspired by Terence Eden's blog post):

Unfortunately, the theme buttons aren't functional on 3DS; everything is readable and navigable, though, and that's good enough for me!
Cursors
One thing I was hoping to do on this site was replace the default cursor with a nifty custom one. Unfortunately, it's harder than it sounds! It's easy enough to just replace all instances of the cursor with your custom cursor, but CSS currently supports 36 predefined cursors. If you want to replace, say, instances of the text cursor with your custom text cursor, and instances of the pointer with your custom pointer, it's just not feasible without essentially implementing custom logic for all cases where a cursor would change! I've created a proposal for the CSS Working Group, the group responsible for making changes to CSS, and maybe if you're reading this in the future, this website will have the custom cursors I always wanted it to have.
Actually Deploying
My laptop runs Nix, but the server I run things on does not! My server uses Docker containers, so what I did was update my flake.nix file, and created a new package that generates a Docker image using the dockerTools.buildImage tool. I then fumbled around with GitHub Actions, something I've never used before, to have a Docker container built and pushed to GitHub's Docker container registry. I'm sure that there's a more efficient way to have done it, but it works! Maybe one day in the future, I'll know more about GitHub Actions(or maybe switch to something like Codeberg), and go back to clean that up.
Final thoughts
Making this site was fun! It was definitely a challenge to make things as idiomatic as possible without resorting to hacky fixes(and I still had to resort to them in some cases), but I think it was worth doing in the end. My biggest concern right now is that some of the art on the site may be a bit heavy for users with poor data connections, but hopefully once prefers-reduced-data is supported by browsers, that'll be less of an issue.
HTML and CSS has gotten a lot more capable over time! If I tried to make this site maybe 5-10 years ago, I'd probably have to resort to some CSS/JavaScript framework, and it'd almost certainly be more difficult to make the site usable for people who don't use JavaScript. I'm looking forward to seeing the web evolve even more over time. All said, you can find the code to this site here.