Hi, I'm Radu

How I built this website

Read time: 9 minutes

Published on: 2024-11-20T14:55:24.000Z

About twice a year I find myself building some sort of web application. This takes me out of my comfort zone - I very much prefer putting together the backend that powers a web application, and leave the front-end side of it to engineers who know what they're doing. And every time, without exception, I realise more and more that front-end is hard… but that doesn't mean I don't have fun doing it!

First things first, here are rough requirements I set for myself:

  • simple design focused on typography
  • a homepage with some key information about myself
  • space to publish articles easily, preferably written in markdown
  • easy to build and maintain
  • fully serverless, ideally no ongoing cost if possible

Design

I am no designer, so I had to get creative in a different way. Probably a bit cliché in 2023, but I asked ChatGPT to suggest a color palette for my website. With that in hand, I plugged those in on coolors and made a couple of adjustments.

Next, typography. I was already set on having a typewriter effect header on the home page, so I had to go for a monospace font. I went on fontjoy and generated a few pairings. I quite liked the well known Roboto Mono, and with that locked, after a few more tries I chose Athiti as the secondary font.

Colours and font in hand, next stop was Figma. It didn't take long to put everything together, for both mobile and desktop. I was quite happy with the result!

Figma View

And then it was time to make it real.

Tech stack

Given that in almost all cases I do this for personal projects, side projects or rough prototypes at work, I typically try to broaden my perspective by trying at least one new frontend thing, be it a framework, an app development platform or tooling. Lately, I had sort of boxed myself in the React / Next / Vercel ecosystem, which sounded appropriate for this project too. However, I had my eye on Svelte for quite some time and never got to try it… until now.

Even though it's been out for a while, Svelte is still not that well known. It has a completely different approach to React - all the hard stuff is done at compile time, and the purists out there are happy to learn that there's no Virtual DOM. In fact, Svelte is a language in itself, but even so I was surprised to see that the learning curve is orders of magnitude less steep than for React. My starting point was not vanilla Svelte however, but SvelteKit, which is to Svelte what Next is to React. Yes, it's great, but more on that in a bit.

Having settled on the base, I needed to find a way to minimise the amount of CSS I have to write, or ideally avoid writing any CSS at all - prime time to pivot from the default Bootstrap to Tailwind. To get going, all I had to do was to import my fonts in app.html and plug my color palette and my fonts into tailwind.config.js:

<link
  href="https://fonts.googleapis.com/css2?family=Athiti:wght@400;500;700&display=swap"
  rel="stylesheet"
/>
<link
  href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500&display=swap"
  rel="stylesheet"
/>
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{html,js,svelte,ts}"],
  theme: {
    extend: {
      colors: {
        transparent: "transparent",
        current: "currentColor",
        black: "#0F0F0F",
        white: "#F4F4F9",
        "electric-violet": "#7B3EFF",
        "light-blue": "#B8DBD9",
        "atomic-tangerine": "#F7A072"
      }
    },
    fontFamily: {
      body: ["Athiti", "sans-serif"],
      monospace: ["Roboto Mono", "monospace"]
    }
  }
};

Frequently going back and forth between my VSCode and the Tailwind docs, I started turning the Figma prototype into reality. I only had four pages:

  • src/routes/+page.svelte - home page
  • src/routes/articles/+page.svelte - page listing all the articles
  • src/routes/articles/[slug]/+page.svelte - each article's page
  • src/404/+page.svelte - pretty self explanatory

For the first three I knew data will eventually be fetched from somewhere, so I set up +page.ts counterparts for each with a load function returning hardcoded objects with the same data as the Figma prototype. By doing this, I made it very easy for myself to move to a real data source when ready.

Navigation

The idea behind the navigation, if it's not obvious enough, is to replicate a directory structure. Given the small number of pages on the website, there are three different scenarios:

  • homepage - the element displays /home it does not link to anything
  • articles page - the element displays /home/articles, where home is a link and articles is not
  • single article page - the element displays /home/articles/<article title>, where home and articles are links, and the article title is not

Global state management in SvelteKit is a breeze. I ended up going for a pattern where each page can define the state of the navigation. To get there, I first created the component with the associated types in src/lib/components/nav.svelte:

<script context="module" lang="ts">
  export type Link = {
    name: string;
    url: string;
  };

  export type NavPath = {
    links: Array<Link>;
    currentPath: string;
  };
</script>

<script lang="ts">
  export let navPath: NavPath;
</script>

<nav class="font-monospace text-base sm:text-lg">
  /{#each navPath.links as linkedPath}
    <span
      ><a
        href={linkedPath.url}
        class="text-atomic-tangerine hover:text-electric-violet transition-colors"
        >{linkedPath.name}</a
      ></span
    >/{/each}<span>{navPath.currentPath}</span>
</nav>

As you can see, the Nav component takes a NavPath object as its only prop, which is made of a number of links and a currentPath. Simple enough. Then I created a store under src/lib/store.ts:

import { writable } from "svelte/store";
import type { NavPath } from "$lib/components/base/nav.svelte";

export const navPath = writable<NavPath>();

Here, navPath will serve as the shared data store, holding the current navigation path used to render the component.

Since the navigation component will be shown on all pages, I added it to src/routes/+layout.svelte as so:

<script lang="ts">
  import Nav, { type NavPath } from "$lib/components/base/nav.svelte";

  let currentNavPath: NavPath;
  navPath.subscribe((value) => (currentNavPath = value));
</script>

<div
  class="bg-black min-h-screen text-white flex min-h-screen h-full w-max-screen pl-2 pr-2 sm:pl-6 sm:pr-6 pt-3"
>
  <div class="grid-cols-1 w-full md:w-4/6 lg:w-3/6">
    {#if currentNavPath}
      <Nav navPath={currentNavPath} />
    {/if}
    <slot />
  </div>
</div>

By subscribing to navPath, we can use the value set by child pages. The downside here is that, theoretically, currentNavPath is reactive even though we don't need it to be, so it probably isn't the best solution but it works well enough.

Deployment

Naturally, the next step after wrapping up the frontend using static data is to figure out where the actual data will come from. Without a decision around where the website will live however, it's hard to make that call given all the requirements I set initially, specifically around wanting to keep this as low cost as possible, even free, and going serverless.

My go-to solution for these situations is DigitalOcean, specifically the App Platform, which has a free tier for up to three static websites. In the end I decided to spice it up a bit.

Firebase sounds great on paper - if you can stomach the vendor lock-in, you get a lot of goodies, such as:

  • a generous free tier
  • multi-platform authentication
  • real time database (Firestore)
  • easy integration with a lot of external tools via extensions (think CRMs, email gateways, you name it)
  • monitoring and analytics

… and a lot more.

So that's what I went for. The initial setup was a breeze. At first, I wrote my own CI on CircleCI (which is also my go-to CI platform, but I'll do an article on that later). I got rid of that very quickly though, as Firebase generates Github Actions automatically, so that took even more away from my hands.

Knowing where the website will be hosted, I was then free to continue the development.

Data

As a first time Firebase user wanting to learn as much as possible in as little time as possible while getting this project out of the door, I jumped in head first without thinking one bit at what the simplest way to tick all the boxes is. What I mean is that I started integrating Firestore.

I created three collections - one for my articles, one for my past jobs and one for my current projects. Fetching data from the database is as simple as it gets, there's nothing interesting to share around that really. The documentation, in typical Google fashion, is very good and the SDK is intuitive. I spent about 10 minutes moving all my data manually, coded the integration and deployed it. Everything was working perfectly.

When it comes to packaging the application, I had made the decision to just generate a static site using SvelteKit's But then it hit me - what would the experience of writing a new post be? Well, I'd have to write it, then insert a new record in the articles collection and then trigger a new build manually in order to include the new article in the static website. Sure, I could build myself a basic admin panel to spin up locally to write articles in, but that's extra work for not much value.

Mind you, at this point I already had all the Firestore queries written:

  • getArticles -> to retrieve all articles ordered descending by publish date
  • getMostRecentArticleSummaries -> to retrieve count article summaries, ordered descending by publish date (an ArticleSummary only includes things like slug, title, description, publishedAt and updatedAt, just enough to render the most recent articles component and the articles page)
  • getArticleBySlug -> to retrieve a single article
  • getSimilarArticleSummaries -> to return a number ArticleSummary objects for articles similar to a given one. Each article has a number of tags, so the query looked for articles that have at least one of the tags present, and filled with other articles in case not enough were found
  • getCurrentProjects
  • getPastJobs

After brainstorming a bit, I realised that having no database also gives me the best user experience. Why not store the articles as markdown files inside the repo directly? This way I can write my articles locally in Markdown, preview the output, raise a PR, see the article deployed in a dev app instance (if I'm too lazy to run npm run dev and check it locally) and then merge. No need for a backend, no need for manually tinkering with database records through the Firestore UI… perfect!

All my data lives in /data:

  • data/currentProject.json - a list of CurrentProjects objects
  • data/pastJobs.json - a list of PastJobs objects
  • articles/<slug> - directory for an article, where I have the following files
  • article.json - this includes a partial ArticleSummary object, made of the title, summary and tags
  • article.md - the article itself

If you followed this closely, you might have noticed that two important article fields are missing: publishedAt and updatedAt. The first option that came to mind was to just store them in the article.json files. But why bother manually formatting ISO timestamp strings when there's an even nicer option, thanks to git! We can use the commit log to find when a file was first committed and when it was last updated, like so:

export const getGitCommitDateForFile = async (
  path: string,
  commit: "first" | "last"
): Promise<Date> => {
  const result = shell.exec(
    `git log --follow --format=%ad --date iso-strict ${path} | ${
      commit == "first" ? "head" : "tail"
    } -1`,
    {
      silent: true
    }
  );

  if (result.code !== 0) {
    throw new Error(`Failed to get ${commit} commit date: ${result.stderr}`);
  }

  if (result.stdout.trim() === "") {
    throw new Error(`Failed to collect ${commit} commit date: no commits found for path ${path}`);
  }
  return new Date(result.stdout.trim());
};

Job done! All that was left was to replace the Firestore queries with variations of fs.readFile(path) calls and small amounts of logic to search and filter.

Wrap-up

There it is! Now you know the inner workings on this website and, more importantly, the iterations that got me here. All in all, it took me about 12 hours of work from idea, through prototype and final product, with some poor decisions in the middle that I shared above.

The key takeaway for me is how great SvelteKit fits to my way of thinking. It feels much more intuitive than React and there is far less you need to know to get productive. Honestly, I think it will be hard for me from now on to go back to React / Next unless there was a good reason to do so. Admittedly, there were a few instances where it was difficult to find answers, a natural consequence of the smaller size of the Svelte ecosystem, but I see a very, very bright future there.

Another important revelation was Tailwind. As someone not up to date with the latest in CSS, and with very limited recent practical experience, Tailwind made me feel like a styling superhero, and it was the main reason why I had so few headaches when recreating the Figma prototype.

You might also like

Using Azure Deployment Stacks

Deployment stacks are a powerful feature of Azure that can bring sanity to complex deployments. In this short article I'll explain what they are by exploring a simple use case.

Launching Yappa, a new productivity tool for freelancers and contractors

I am pleased to announce the launch of Yappa, a powerful productivity tool designed by a freelancer, for freelancers. Yappa helps streamline client management, simplify time tracking, and more - all in one place. In this article I share the story behind Yappa and give a technical sneak peek into how it was built.

Fixing OpenAI and Anthropic initialization errors when using Newrelic Agent in ESM projects

Using Newrelic’s suggested approach to initialization on ESM projects can sometimes break packages like openai-node and anthropic-ai. This guide provides a simple workaround using a custom loader to exclude these packages from being intercepted, ensuring your application runs smoothly and Newrelic Agent bootstraps correctly.

Who I am

I'm a seasoned software engineer on a mission to help startups take flight, because in today's world good tech is the difference between disruptor and... well, disrupted.


Want to build a winner? Let's chat!