Project section facelift

I was recently looking at the kotlin-results library which describes it self as a “multiplatform Result monad for modelling success or failure operations”.

This library is great in combination with Kotlin Flow’s (or RxJava), and I would highly recommend checking it out.

At some point though I ended up on the author’s personal webpage and specifically noticed their projects section, which I found to be especially well organized:

A screenshot of one of Micheal Bull's project listings
A very clean presentation of his project 'kotlin-result'

My own project section was at this time, let us say, less than ideal. Each project listing was simply written out in markdown, all rather dull and static. It needed to get a facelift.

Which I gave it and now it looks almost identical to Micheal Bull’s:

A screenshot of one of my new project listings
A very clean presentation of my project 'waktatime-client'

What I am even more happy about is that this section is now automatically built from data pulled from Github and there is no manual work required on my part to update the section.

What follows is a detailed rundown of what was done.

The source data

To begin with I wanted to have the information needed pulled from Github rather than having to compile it manually.

It so happens that Github has an open API available for pulling repository information by user name https://api.github.com/users/<user-name>/repos which contains pretty much all the data one could want for this kind of project.

As the page is hosted on Github, there is also an Github action that builds the page each time there is a pull request made into main. Adding a step to that action that downloads the repository information was as simple as doing the following (other parts omitted):

name: Build and deploy page
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Fetch, build and deploy page
    runs-on: ubuntu-20.04
    steps:
      
      # Beginning of build script
      ...

      # Here the repositories are fetched as a part of the build process 
      - name: Get repositories
        uses: wei/wget@v1
        with:
          args: -O ./data/repositories.json https://api.github.com/users/hrafnthor/repos 
      
      # Here I added a quality gate to check for the existence of the downloaded file 
      - name: Check for repositories.json existence
        uses: andstor/file-existence-action@v1
        with:
          files: "./data/repositories.json"

      # In case the quality gate was unable to find the downloaded file, exit with a failure
      - name: Fail if repositories.json does not exist
        if: steps.check_files.outputs.files_exists == 'false'
        run: |
          echo "File ./data/repositories.json was not found!"
          exit 1          

      # Rest of build script
      ...

The repository.json file is now available for use.

Now all that was needed was to process the data, and for that Hugo already has the perfect tool.

Hugo shortcodes and partials

Hugo uses Markdown in the creation of its pages, and in an attempt (which in my opinion succeeded) to circumvent having to add raw html elements directly to the markdown, when such a thing is needed, the shortcodes were created.

In short, they are predefined snippets that can be referenced inside of the markdown and will perform various templating actions at build time.

While shortcodes can be used along side html and markdown, partials can not and are more restricted in their access of the outer scope. The way I have utilized these elements is to build my shortcodes out of various partials along with html elements.

For more information than I will go into on both of these, see the official documentation.

Bringing it all together

Under the /layouts/shortcodes path I created a github-repositories.html shortcode that contains the relevant CSS, svg elements (borrowed directly from Micheal Bull’s page source) as well as the shortcode it self:

<style type="text/css">
  .repositories {
    display: flex;
    flex-wrap: wrap;
    padding: 0 2px;
  }
  .repository {
    border-style: solid;
    border-width: 2px;
    border-color: #e1e4e8;
    border-radius: 10px;
    padding: 10px 20px 20px 20px;
    width: 400px;
    margin: 10px;
    background-color: white;
  }
  .repository .description {
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    color: #586069;
    height: 60px;
  }
  .repository [class^="icon"] {
    width: 14px;
    height: 14px;
  }
  .repository .details {
    display: flex;
  }
  .repository .circle {
    margin-right: 4px;
    height: 16px;
    width: 16px;
    border-radius: 50%;
  }
  .repository .circle.kotlin {
    background-color: #F18E33;
  }
  .repository .circle.python {
    background-color: #3572A5;
  }
  .repository .circle.shell {
    background-color: #89e051;
  }
  .repository .circle.html {
    background-color: #e44b23;
  }
  .repository .circle.javascript {
    background-color: #f1e05a;
  }
  .repository .circle.typescript {
    background-color: #2b7489;
  }
  .repository .circle.java {
    background-color: #b07219;
  }
  .repository .circle.ruby {
    background-color: #701516;
  }
  .repository .circle.php {
    background-color: #4F5D95;
  }
  .repository .circle.perl {
    background-color: #0298c3;
  }
  .repository .circle.dart {
    background-color: #00B4AB;
  }
  .repository .circle.c {
    background-color: #555555;
  }
  .repository .circle.swift {
    background-color: #ffac45;
  }
  .repository .circle.lua {
    background-color: #000080;
  }
  .repository .circle.clojure {
    background-color: #db5855;
  }
  .repository .circle.rust {
    background-color: #dea584;
  }
  .repository .circle.coffeescript {
    background-color: #244776;
  }
  .repository .circle.go {
    background-color: #375eab;
  }
  .repository .circle.groovy {
    background-color: #e69f56;
  }
  .repository .combo {
    padding-right: 25px;
    display: flex;
    flex-direction: row;
    align-items: center;
  }
  .repository .updated {
    color: #586069;
    font-size: 12px;
  }
  .icon-book {
    margin-right: 4px;
  }
</style>

<svg style="position: absolute; width: 0; height: 0;" width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <symbol id="icon-star" viewBox="0 0 16 16">
      <<title>star</title>
        <path fill="#586069", fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
    </symbol>
    <symbol id="icon-book" viewBox="0 0 16 16">
      <title>book</title>
      <path fill="#586069",fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
    </symbol>
    <symbol id="icon-fork" viewBox="0 0 16 16">
      <title>fork</title>
      <path fill="#586069", fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
    </symbol>
  </defs>
</svg>

{{ if os.FileExists "/data/repositories.json" }}

  {{ $repositories := index .Site.Data.repositories }}

    <div class="repositories">
      {{ range $repositories }}
        {{ if not .fork }}
          {{ partial "repository" . }}
        {{ end }}
      {{ end }}
    </div>
{{ end }}

Here the repository file is iterated upon, if discovered, and the partial called with each item from the list. As a simple solution for assigning the right color scheme per programming language, I simply encoded some of the main language colors in CSS and then assign class names based on languages inside the partial.

Speaking off, the repository partial looks like this:

<div class="repository">
  <div class="combo">
    <a href="{{ .html_url }}" target="_blank">
      <svg class="icon-book">
        <use xlink:href="#icon-book"></use>
      </svg>
      <span>{{ .name }}</span>
    </a>
  </div>
  <div>
    <p class="description">{{ .description }}</p>
  </div>
  <div class="details">
    <div class="combo">
      <div class="circle {{lower .language }}"></div>
      <span>{{ .language }}</span>
    </div>
    <div class="combo">
      <a href="{{ .html_url }}/stargazers" target="_blank">
        <svg class="icon-star">
          <use xlink:href="#icon-star"></use>
        </svg>
        <span>{{ .stargazers_count }}</span>
      </a>
    </div>
    <div class="combo">
      <a href="{{ .html_url }}/network/members" target="_blank">
        <svg class="icon-fork">
          <use xlink:href="#icon-fork"></use>
        </svg>
        <span>{{ .forks_count }}</span>
      </a>
    </div>
  </div>
  <div class="updated">
      <span>{{partial "updated-since" .updated_at}}</span>
    </div>
</div>

You may notice that I reference another partial at the bottom of the repository partial.

Since the Github data gives raw dates, they needed to be parsed to human readable values. And rather than having it inside the repository partial I thought it might be better to make it more reusable, by extracting it into another partial.

The updated-since partial looks like this:

{{ $delta := now.Sub (time .) }}
Upated 
{{ if (lt $delta.Hours ($yearlyHours := (mul 365 24)))}}
    <!-- Less than a year -->
    {{ if (lt $delta.Hours ($monthlyHours := (mul 30 24)))}}
        <!-- Less than a month -->
        {{ if (lt $delta.Hours 24 )}}
            <!-- Less than a day -->
            {{ $delta.Hours }} hours 
        {{ else }}
            {{ int (div $delta.Hours 24) }} days
        {{ end }}
    {{ else }}
        {{ int (div $delta.Hours $monthlyHours )}} months
    {{ end }}
{{ else }}
    {{ int (div $delta.Hours $yearlyHours) }} years
{{ end }}
ago

Then the shortcode is simply referenced inside the projects page, which will cause it to be run at build time.

Conclusion

As I am no web developer, there may be plenty wrong with my implementation. For one I have CSS definitions inside my shortcodes, and this may not be best practice. However as they are strictly related to the shortcode it self, I feel it makes sense to have them there. This way it is also simpler and works.

In the end I am rather pleased with the results, and it is clearly a big improvement over how my project section looked before the change.

It was also fun to dive a bit deeper into Hugo’s shortcodes and tinker with them. I can see a lot of potential use cases for them in future projects.