Samuli Kärki

Week 03: Project Management

Our assignment for Week 3 was to:

I'm going to start by describing how I got my GitLab repository and local Git set up, and then move on to describing how I created my website.

Setting up your GitLab repository

To get started, we need to create a new repository in GitLab, which is where our code will live. To do this, click on the "New project"-button on the GitLab Projects overview -page:

A screenshot of the GitLab New Project -button displayed on the user overview page

After this, you are greeted with a form. We were instructed to enter digital-fabrication as the name for our repo, so that is what we are going with.

At this point it's also important to set the repository to public, so that the teachers and other classmates can view it. Rest of the settings don't really matter here, I opted to create a as it's what I'm used to.

A screenshot of the GitLab New project -form

Your repo should now be created, and you should be greeted with a view of your repository.

A screenshot of the overview page of our new GitLab project

At this point it's also a good idea to create an SSH key that GitLab can identify you with. This means that you don't have to enter your GitLab username and password each time you try to push changes to the remote, which can quickly become quite tedious.

To do this, we first need to generate an SSH key on our machine. Open the terminal and run the following command:

$ ssh-keygen -t ed25519 -C "your_gitlab_email_here"

Please note that the $ is not a part of the command, but there just to denote which lines are meant to be typed in the terminal

ssh-keygen will prompt for a passphrase, I used one generated by my password manager. Please use something secure.

After this, the SSH keypair is now generated. Now we need to copy the public key that GitLab can use to our clipboard. To do this easily, run:

$ tr -d '\n' < ~/.ssh/ | pbcopy

Now head on over to Edit profile > SSH Keys. You should be greeted with the following view:

A screenshot of the initial state of the SSH keys form

Here we can paste in your SSH key we copied to your clipboard in the previous step. Be sure to add a descriptive name, especially if you are accessing Git on multiple machines. I usually name them after the device the key is for, i.e. "Macbook Pro"

Press "Add key" and you should be good to go!

To finish setting up the SSH key, we also need to the key we just generated to the ssh-agent running on your machine:

# start the ssh-agent in the background
$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/id_ed25519

Most of the GitLab and Git setup is now out of the way, we just need one more step to configure Git on our local machine. To do this, open a terminal, and type in the following commands:

$ git config --global "Your Name"
$ git config --global "your_gitlab_email"

This makes sure that GitLab can properly attribute commits to our GitLab user in the remote repository that we created earlier.

Now we can clone our repository by opening the terminal and typing:

$ git clone<your_gitlab_username_here>/<your_repository_name_here>.git
$ cd <your_repository_name_here>

Now if you run ls -a, you should get output that looks something like the following:

$ ls -a
.  ..  .git  .gitlab-ci.yml

Static Site Generation

To create our websites, we were instructed to use a static site generator, and deploy it to GitLab pages.

At first, I went with 11ty, as I'm quite familiar with JavaScript (and hate it with a passion. Talk about a Stockholm syndrome) and I've dabbled with it a before, so getting it set up would be a breeze. Right?


To get started, I needed node. To do this, I headed over to and downloaded the LTS version for macOS.

After installing, I reopened my terminal and checked that the installation was succesful with

$ node --version

After installing node, I headed over to the digital-fabrication repository I had just cloned on my machine, and ran the commands they detail under their Quick Start -section on their front page:

$ npm install -g @11ty/eleventy
$ echo '# Page header' >
$ eleventy

After this I ran eleventy --serve and headed over to http://localhost:8080/README/. I was greeted with the following view:


Wowzers, I had my own website! After this I dived deep into the docs to create my own templates (all the tutorials use Nunjucks, so this is what I ended up going with) and enabling file passthrough to serve static files alongside with my website.

Getting rid of 11ty

During one lecture, Kris mentioned that he doesn't really like 11ty and other similar static site generators due to the amount of dependencies they have, and I must say I agree. Kris even mentioned an alternative to Hugo with minimal dependecies called Zola.

I had to think about the switch for a while, but if I could choose between having a single minimal binary and a huge javascript blob, the choice was simple. Even the only bonus with 11ty, which was that I was familiar with JavaScript, didn't seem to matter much, as I was mainly writing Markdown and HTML with the exception of the configuration file, so I ended up switching to Zola.


To install zola, I used Homebrew, as I'm on mac:

$ brew install zola

After this I initialized my site by navigating into the git repository I cloned earlier and using the following command:

$ zola init --force

This will initialize zola in the folder you are currently in (i.e. the git repository). The --force option is required, as by default Zola doesn't allow initialization in a non-empty folder.

The installation prompted me the following:

> What is the URL of your site? (
> Do you want to enable Sass compilation? [Y/n]: Y
> Do you want to enable syntax highlighting? [y/N]: N (at this time I didn't know I'd be adding code snippets to the site)
> Do you want to build a search index of the content? [y/N]: N

After this the default directory listing looked like the following:

$ ls -a
.   .git     sass    templates  .gitlab-ci.yml  config.toml
..  content  static  themes

Using Zola

For the sake of brevity, I won't detail too much of the differences between 11ty and Zola, but will instead focus on Zola, and how static site generators work in general.

The idea behind static site generators is that you write content in Markdown (.md-files), which then gets combined with templates and converted into HTML.

Templates specify how your content gets laid out. Templates in Zola live under the folder templates. Here's the template of my website (templates/index.html) as an example:

{% block content %}
<!DOCTYPE html>
<html lang="en">
   <meta charset="utf-8">
   <link rel="stylesheet" href="/main.css">
   <title>{{ section.title | safe }}</title>
      <a class="title" href="/">Samuli Kärki</a>
          <li><a href="/assignments">Assignments</a></li>
          <li><a href="/final-project">Final Project</a></li>
          <li><a href="/reading-list">Reading List</a></li>
     {{ section.content | safe }}
{% endblock %}

You can see here that the actual written content of the Markdown file (section.content) together with a title (section.title) defined in the metadata or "front matter" of the Markdown file. Markdown content in Zola lives under the folder, you guessed it, content. Here's an example of a file (content/assignments/ that uses the template above:

title = "Assignments"
template = "index.html"

# Assignments

- [Assignment 03: Project Management](/project-management)

Here you can see the front matter, which is denoted by wrapping in it two lines consisting of +++. In the front matter we define what the title of the page is going to be, and what template we are going to use. Here the title, in this case "Assignments", gets passed to section.title in the template.

After the front matter comes the actual content, which gets passed to section.content in the template. Markdown lets you format content in different ways. Here the content consists of a header titled "Assignments", followed by a list with one entry, which is a link to this page you're currently reading. More on Markdown formatting here.

When rendered, that is the Markdown content gets combined with the template into a .html file, the result looks like this:

Screenshot of the webpage rendered by Zola


The way routing works in Zola is that each subfolder in the content folder generates a new route. As an example, the following directory structure:

├── content
│   └── blog
│   │   └──
│   ├──

would generate the following routes (with the base url of the site being localhost:8080):

localhost:8080/blog files in Zola are called "sections". There is also support for "pages", where you have multiple files in a single content folder, but I've yet to use this feature, so I can't say much about it.

As of 9th February 2022, I've now moved the "Assignments" part of my page to use pages. This means that my assignments folder looks like the following:


To get this working properly, the section ( front matter needed some changing:

title = "Assignments"
template = "assignments.html"
page_template = "assignment-page.html"

Here the page_template defines what template to use for each file in the assignments folder that is not named, i.e. is a page and not a section. A separate template is needed for the pages, not only because pages use different template variables, but also because I wanted a Table of Contents on the assignments page, but not on other pages. The assignment-page.html template looks like the following:

{% extends "base.html" %}

{% block content %}
<h1 class="title">
    {{ page.title }}
{% if page.toc %}
    {% for h1 in page.toc %}
            <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
            {% if h1.children %}
                    {% for h2 in h1.children %}
                            <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
                            {% if h2.children %}
                                    {% for h3 in h2.children %}
                                            <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
                                    {% endfor %}
                            {% endif %}
                    {% endfor %}
            {% endif %}
    {% endfor %}
{% endif %}
{{ page.content | safe }}
{% endblock content %}

Before, I only had a single index.html template, but now I've moved the top-level HTML to it's own base.html template, and now index.html handles displaying content on all section pages.

For the "Assignment" section, I needed to extend index.html so that it would list all the pages in the assignments section. The template was looks like this:

{% extends "index.html" %}

{% block content %}
    <h1 class="title">
        {{ section.title }}
        {% for page in section.pages %}
            <li><a href="{{ page.permalink | safe }}">{{ page.title }}</a></li>
        {% endfor %}
{% endblock content %}

In addition I needed to change the base.html template so that it would also display page titles:

    {% if section.title %}
      {{ section.title }}
    {% endif %}
    {% if page.title %}
      {{ page.title }}
    {% endif %}

Overall the transition to pages was pretty painless, and it made organization easier. Now I don't have to manually maintain the list of assignments, and I can also modify the assignment themselves to look different if needed, now that they have a separate template.


Styling websites is traditionally done with CSS, but Zola has support for SASS. SASS extends CSS with some useful features, more on which below. SASS files in Zola live under the folder sass, unsurprisingly. When you build your website, the SASS files get compiled into standard CSS: main.scss becomes main.css and so on.


SASS enables us to import other SASS files. This is especially useful for organization. In Zola (and I think in SASS general), files that are meant to be included with others are prefixed with a _. For example, I have all my variables in a file called _variables.scss, which then gets imported in my main.scss like so:

@import '_variables';

Now all the variables I've defined in _variables.scss are now accessible in main.scss as well.

In Zola, SASS files prefixed with a underscore don't get put into the public folder upon building the website.


Variables provide a useful way of making CSS a bit more easier to write and read. They're especially useful for color data. For example, my _variables.scss has the following line:

$background-color: #fff;

Which I then use in main.scss like so:

background: $background-color;

This means that if I choose to change my background color one day, I only have to change the one line in _variables.scss, instead of going through my main.scss and changing every line where I use white as the background color.

Code organization

SASS also allows us to organize CSS blocks more clearly. While I could normally write

body {
    background: $background-color;
    max-width: 73%;

body a {
    border-bottom: 1px solid black;
    color: black;
    text-decoration: none;

body a:hover {
        border-bottom: 0;

to scope these blocks to only affect the body of my website, it gets old pretty fast. SASS allows us to write

body {
    background: $background-color;
    max-width: 73%;

    a {
        border-bottom: 1px solid black;
        color: black;
        text-decoration: none;

    a:hover {
        border-bottom: 0;

which is a lot less tedious to write, and also is easier to read due to the indentation providing clearer context for which parts of the site which CSS blocks affect.

Serving files

In Zola, files are served from the static folder. No configuration is needed for this. Organization inside the static folder is free, for example I have the following structure:

├── files
│   └── building_a_diy_transmitter.pdf
└── img
    ├── anime-book.gif
    ├── final-project.gif
    ├── new_project.webp
    ├── on_distance.MOV
    ├── on_distance.jpg
    ├── pipelines.webp
    ├── project_form.webp
    ├── project_overview.webp
    ├── rendered_webpage.webp
    ├── selfie.gif
    ├── ssh_keys.webp
    ├── ssh_keys_initial.webp
    └── ssh_keys_small.webp

All these files then get copied into the public folder upon building your Zola website, and can be accessed the same way you would access them during development. For example, the url for my glorious selfie is simply /img/selfie.gif, no need to add the static.

Media optimization

For image optimization, I resized most images to 1000 pixels wide, which seemed large enough for most images on this site. Images with smaller widths I kept as is. Here you can see the disk usage of my different visual media files:

du -h static/img/*
1.3M    static/img/anime-book.gif
8.0K    static/img/final-project.gif
20K     static/img/new_project.webp
2.2M    static/img/on_distance.MOV
2.0M    static/img/on_distance.jpg
12K     static/img/pipelines.webp
88K     static/img/project_form.webp
36K     static/img/project_overview.webp
20K     static/img/rendered_webpage.webp
12K     static/img/selfie.gif
44K     static/img/ssh_keys.webp
28K     static/img/ssh_keys_initial.webp
20K     static/img/ssh_keys_small.webp

All the GIF files are dithered and only use two colors, black and white. IMO, this gives a nice stylized effect, while still being mostly readable. I wouldn't use it on images that require a lot of fine details to be visible, but it works fine in this case.

For images that do require a lot of fine details, I ended up going with WebP. I have mixed feelings about this format: it's supported by most modern browsers, yet most if not all operating systems seem incapable of displaying them, and instead open the browser when you want to view them. And it's made by Google, and we all know what we think about Google.

But it compressed really well, as you can see. Google provides the cwebp tool for converting images to WebP, but I instead opted to use the Photoshop plugin WebPShop, so that I could have the conversion as part of my resize workflow, which I also do in Photoshop. One could probably also just write a bash script that uses imagemagick and cwebp to do all the resizing but I'm too lazy to do that...

As for the biggest files in the static folder, anime-book.gif is an anime GIF (unsurprisingly), and I love them anime GIFs. I will probably try to compress it with ffmpeg soon, or give it the 2-color dither treatment to see what it looks like.

The other two, titled on_distance.jpg and on_distance.MOV are part of my Electronics for Artists -documentation. I initially just uploaded them to have the documentation submitted by the deadline, but now I'll have to tackle the compression as well. As for on_distance, I think I'll give it the WebP treatment, and for the movie, I'll either compress it using ffmpeg or convert it to WebM. I'll have to see which gives better results.

Update 9th Feb 2022: After encountering the need to batch resize and convert images to .webp (due to the large amounts of screenshot generated for the Week 04 assignment), I've been using the following script, copied from StackOverflow and modified to my own needs:


for file in *.png ; do cwebp -q 90 -resize 1000 0 "$file" -o "${file%.png}.webp"; done

Serving our website

After handling all the boilerplate and getting Zola actually up and generating web pages, it was time to deploy it to GitLab Pages.

Before pushing to git, I felt like it was a good idea to create a .gitignore file to prevent unwanted files from being pushed into the remote. Mine looked like the following:


To actually deploy to GitLab Pages, the .gitlab_ci file needed modifying. This is the file that tells the GitLab CI/CD (Continuous Integration/Continuous Deployment) pipeline what commands should be run at what stage. Luckily, the folks at Zola had already done most of the work for me by providing a readymade .gitlab_ci file. The only changes I did to this file were removing the submodule stuff, because I don't use themes, and changing the master branch to main instead:

image: alpine:latest

    # Install the zola package from the alpine community repositories
    - apk add --update-cache --repository zola
    # Execute zola build
    - zola build

      # Path of our artifacts
      - public

  # This config will only publish changes that are pushed on the master branch
    - main

After this we are ready to commit and push:

$ git add -A
$ git commit -m "Deploy to GitLab Pages"
$ git push

After this I headed to the GitLab CI Pipelines -section, where the initial run of the pipeline was failing. I was a bit perplexed, but turns out it was failing because I hadn't verified myself using a credit card. This is apparently a precaution taken by GitLab to prevent abuse of their free CI/CD runners.

After verifying myself, I re-ran the pipeline and was greeted with the following blissful view:

A screenshot of the GitLab CI pipelines overview

For a second I thought of adding a screenshot of my webpage here to show you that the deployment was succesful, but the fact that you are reading this is probably enough proof of that.