little-things Website

During 2022 I set out to redesign and rebuild my personal website from the ground up. The redesign had several goals:

  • make the site responsive
  • give it a facelift to look fresh and modern
  • modernize the tech stack and simplify the technology used
  • offer a single home for both my personal stuff and portfolio

With these goals in mind I decided to use Bootstrap for the styling, as it’s the industry standard at the current time, meaning it’s proven and stable. For the website content I chose the Astro static site generator, because it offers great flexibility and produces a fast, optimized website.

Screenshots

Insights

Architecture

In this section I will give an overview of how I rebuilt the site using Astro, with condensed code examples that demonstrate how the systems I needed for the website were implemented. Omissions, to not bloat this article too much, are marked by (...) in the code.

The Content

This website is built around two content collections:

  • News
  • Projects

For the content entries I rely on MDX, an extended variant of Markdown. Astro also offers schema validation for the MDX frontmatter, which is a welcome safety net for accidental errors.

Since the news collection implementation is basic and very similar to the projects collection implementation, I will demonstrate only the projects’ system going forward as it offers more bells and whistles. Following is a condensed example of the frontmatter metadata for the Alien Invasion project:

---
layout: "/src/layouts/ProjectLayout.astro"
title: "Alien Invasion"
description:
  "A space invaders-like shoot 'em up game developed with C# and DirectX 10, featuring some fancy 2D graphics."
date: 2015-01-11
categories:
  - Featured
  - Game Dev
  - Programming
coverImage:
  path: "/src/assets/projects/game-dev/alien-invasion/cover.png"
license:
  name: "CC BY-NC 4.0"
  url: "https://creativecommons.org/licenses/by-nc/4.0/"
  icons:
    - "cib:creative-commons"
    - (...)
tools:
  - { slug: "dot-net", version: "4.6.1" }
  - { slug: "fmod" }
  - (...)
programmingLanguages:
  - { slug: "csharp", version: "6" }
linkBox:
  title: Downloads
  links:
    - { name: "Alien Invasion 1.0", url: "/files/projects/game-dev/alien-invasion/Alien Invasion v1.0.zip" }
    - (...)
---
(Content starts here...)

Projects

To list all the projects on the overview page I can simply query the content collection called projects and iterate over each project using a component called ProjectCard. That component renders each indivudal entry by using the data passed in as a parameter of the project files’ frontmatter. The grid layout is defined in a div wrapping this loop.

Pagination is handled completely by Astro for me. This can be seen in the getStaticPaths function inside the code fence. For this to work, all you have to do is call the paginate function and giving it the collection of MDX files to paginate and the page size. Information about the current page and total pages can then be read from the Astro.props, which is a special object that holds all paramters being passed to components or Astro pages:

---
import ProjectOverviewLayout from "@layouts/ProjectOverviewLayout.astro";
import ProjectCard from "@components/projects/ProjectCard.astro";
import { CollectionEntry, getCollection } from "astro:content";
import { sortProjectsByDate } from "@scripts/projects";

export interface Props {
  page: any;
}

export async function getStaticPaths({ paginate }: any) {
  const allProjects = await getCollection("projects");
  const sortedProjects = sortProjectsByDate(allProjects);
  return paginate(sortedProjects, { pageSize: 9 });
}

const title = "Projects";
const { page } = Astro.props as Props;
const pageNumbers = Array.from({ length: page.lastPage }, (_, i) => i + 1);
---

<ProjectOverviewLayout title={title}>
  <div class="row row-cols-1 row-cols-lg-2 row-cols-xl-3 g-4">
    {page.data.map((project: CollectionEntry<"projects">) => <ProjectCard project={project} />)}
  </div>

(...)

The ProjectCard component is rather simple so I won’t give a code example here, a brief description shall suffice instead: it takes the given project parameter, retrieves the necessary metadata from its frontmatter and renders it in a Bootstrap card component.

Project Page

The project page heavily relies on components. One example is the project info box that gives a quick overview of the project and the tools used. Since I didn’t want to repeat the information about each tool, like icon and URL, again and again on every single project entry, I wrote a simple tools system that is also used to display the programming languages used in the respective projects. The system provides default values but allows for them to be overridden on a case-by-case basis for individual projects. Here is a condensed version of it:

import type { Tool } from "little-things";

// The tools are actually stored in a JSON data file and included here for brevity
const tools: Tool[] = [
  {
    slug: "affinity-designer",
    name: "Affinity Designer",
    icon: "vscode-icons:file-type-affinitydesigner",
    url: "https://affinity.serif.com/designer/",
  },
  {
    slug: "affinity-photo",
    name: "Affinity Photo",
    icon: "vscode-icons:file-type-affinityphoto",
    url: "https://affinity.serif.com/photo/",
  },
  (...)
];

export function enrichTool(tool: Tool | undefined): Tool {
  if (tool === undefined) {
    throw new Error("Tool must not be undefined");
  }

  const foundTool = tools.find((t) => t.slug === tool.slug);
  if (!foundTool) {
    return tool;
  }

  if (tool.version) {
    foundTool.version = tool.version;
  }
  if (tool.url) {
    foundTool.url = tool.url;
  }
  if (tool.icon) {
    foundTool.icon = tool.icon;
  }

  return foundTool;
}

(...)

The data and function for programming languages is mostly identical, so it has been omitted in the code listing above. Both data and functions are then used in the ProjectInfoBox component that iterates over the tools listed in the projects’ MDX frontmatter and renders out a tools entry using another component called ToolsElement:

(...)
        {
          frontmatter.tools && (
            <li class="list-group-item ps-0">
              <strong title="The tools are listed in descending order of their usage, more used tools are first.">Tools used</strong>
              <ul class="list-inline ps-4">
                {frontmatter.tools.map((tool: Tool) => (
                  <ToolElement tool={enrichTool(tool)} />
                ))}
              </ul>
            </li>
          )
        }
(...)
Categories

The project categories are read from the project MDX frontmatter and collected in a set, meaning there is no central definition of all categories, instead they are defined by the projects. That way there will never be an empty category. As before, pagination and the catgory overview pages and handled by Astro and generated fully dynamic based on the project MDX files that exist in the projects content collection.

The All and Featured categories are special categories. All is not listed on any project entry (as it would have to be listed on all projects, which is only a vector for potential mistakes) and Featured is filtered out for the project overview cards. To make working with them easier I defined an enum, to have type safety and prevent typos, and a function to filter the Featured category out of the frontmatter, to have it defined in a central place in case there may be other special categories that need to be filtered out in the future.

export enum SpecialCategory {
  All = "all",
  Featured = "featured",
}

export function getCategories(projects: CollectionEntry<"projects">[]): Category[] {
  const categoriesSet = new Set<string>();
  projects.forEach((project: CollectionEntry<"projects">) => {
    project.data.categories.forEach((category: string) => {
      categoriesSet.add(category);
    });
  });
  return generateCategoryData(categoriesSet);
}

export function getFilteredCategories(categories: string[]): string[] {
  return categories.filter((x) => x.toLowerCase() !== SpecialCategory.Featured);
}

Backstory

Before the relaunch of this website I ran two websites: a personal website and a portfolio powered by Wordpress. The personal website was from the 2000’s and still used frames (yes, frames not iframes!) The thought to redo this outdated website with modern technology lingered for a long time in the back of my head. Furthermore, I wanted to get rid of Wordpress, on the one hand due to the countless security holes and hacks Wordpress became infamous for and on the other hand the software became an overused complexity monster in my eyes, that far exceeded what I needed for my site. I wanted something simple, yet I wanted to keep some comfort features that come with a dynamic website. Lastly, I wanted to move to a single, consolidated website instead of two separate ones.

Jumping forward to 2022, I stumbled over the concept of static site generators (SSG) and it was love at first sight. A SSG combines the advantages of using a programming language allowing for dynamic generation, reusing components, pagination without getting a headache and so on while having a plain HTML, CSS and possibly JavaScript website as an end product.

First Steps

At first I was using an SSG called eleventy (or 11ty) and created most of the website utilizing Bootstrap for the styling. One gripe I had with 11ty was the templating language: Nunjucks. While being capable and powerful, it messed with the linting and auto-complete of the HTML files. I didn’t get a setup to work that supported both HTML and Nunjucks, so I had to constantly switch the VS Code Language Mode from on to the other, which became annoying real fast.

Enter Astro

Then I learned about Astro. With Astro you simply use JavaScript (or TypeScript, what I liked even more) in your HTML files. It’s a minor thing, but one that makes your daily work noticably smoother. I am also in favor of how Astro handles content with Markdown files and the idea of component islands in case you want to enhance your static website with dynamic content.

Migrating the website from 11ty to Astro was done in a couple of days, the layout and styling could be reused for the most part and the dynamic template commands were easy to convert to TypeScript.

Attributions

Special Thanks

Special thanks go to Daniel Friedenberger for giving me constructive feedback on everything regarding design and layout on this website. Otherwise this would look like programmer art. 😁