- Date: 9.10.2022
- Categories
- Programming languages
- Tools used
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
What is the Frontmatter?
The frontmatter is metadata written in YAML and delineated from the markdown content by wrapping it between ---
at the top and bottom. It is placed at the beginning of a markdown or MDX file, before the content, and its properties can be queried and used in the markdown itself and in other Astro files.
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.
What is a Code Fence?
A code fence is the area to write code that is put between the ---
characters at the top of a file. The code is executed offline to build the website.
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. 😁