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 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 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 Link to heading
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.
So I wrote a small script that fetches the list of projects and stores them for further use:
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# This script fetches a list of public repositories and stores them to the
# requested location. It expects the location path to be supplied as input.
# -----------------------------------------------------------------------------
set -e
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
info () {
echo -e "${BLUE}$1${NC}" >&2
}
error () {
echo -e "${RED}$1${NC}" >&2
}
# Fetches a specific page of public repositories, and stores them under /tmp/repositories/.
#
# Expects a single input indicating the page number being fetched.
fetch_page () {
if [ $# -ne 1 ]; then
error "${BASH_SOURCE[0]}, lineno: ${LINENO}: Function expects a single input parameter, the page number!"
exit 1
fi
local page output_path
page=$1
output_path="/tmp/repositories/${page}.json"
info "Fetching page number ${page} and outputting to ${output_path}"
curl --silent --dump-header /tmp/headers --request GET "https://api.github.com/users/hrafnthor/repos?page=${page}" -o "${output_path}"
}
# Recursively initiates calls to fetch_page() until all pages have been exhausted.
#
# Expects a single input, indicating the current page being fetched.
fetch () {
if [ $# -ne 1 ]; then
error "${BASH_SOURCE[0]}, lineno: ${LINENO}: Function expects a single input parameter, the batch number!"
exit 1
fi
local batch
batch=$1
fetch_page "$batch"
local next_page
next_page="$(grep -i '^link:' /tmp/headers | sed -n 's/.*<\([^>]*\)>; rel="next".*/\1/p')"
if [ -n "$next_page" ]; then
info "Sleeping for 5 seconds"
sleep 5
batch=$((batch + 1))
fetch $batch
else
info "No more pages left"
fi
}
execute () {
if [ $# -ne 1 ]; then
error "${BASH_SOURCE[0]}, lineno: ${LINENO}: Script expects to receive the absolute path to where the repositories should be stored as input!"
exit 1
fi
local output_path
output_path="$1"
mkdir -p "/tmp/repositories"
fetch 1
info "Merging json files in '/tmp/repositories' into '${output_path}'"
jq -s 'add' /tmp/repositories/*.json > "$output_path"
}
execute "$@"
Then I simply hooked this script up to the CI/CD pipline so that it downloads the repository information on each run:
name: Build and deploy page
on:
push:
branches:
- main
jobs:
deploy:
name: Fetch, build and deploy page
runs-on: ubuntu-20.04
env:
REPOSITORY_JSON_PATH: './data/repositories.json'
steps:
# Relevant parts of the build script ...
- name: get repositories
run: ./scripts/fetch_repos.sh "$REPOSITORY_JSON_PATH"
- name: Check for repositories.json existence
uses: andstor/file-existence-action@v1
with:
files: $REPOSITORY_JSON_PATH
- name: Fail if repositories.json does not exist
if: steps.check_files.outputs.files_exists == 'false'
run: |
echo "File ${REPOSITORY_JSON_PATH} was not found!"
exit 1
# Rest of build script ...
The whole combined repository.json
file is now available for further use.
Now all that was needed was to process the data, and for that Hugo already has the perfect tool.
Hugo shortcodes
and partials
Link to heading
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 instance almost all the code snippets in this post use a shortcode
I created to embed the file contents directly into the post. This means the script contents will be up to date as the page evolves. And here is the content of that particular shortcode
displayed, interestingly, using it self:
{{/*
Shortcode: code
Usage:
{{< code language="bash" source="/scripts/fetch_repos.sh" >}}
*/}}
{{ $language := .Get "language" }}
{{ $source := .Get "source" }}
{{ with $source | readFile }}
{{ highlight (trim . "\n\r") $language }}
{{ end }}
For more information than I will go into on both of these, see the official documentation.
Bringing it all together Link to heading
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: 1px;
border-color: gray;
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.jinja {
background-color: #a52a22;
}
.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.shell {
background-color: #89e051;
}
.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 | default "shell" }}'></div>
<span>{{ .language | default "Shell" }}</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 Link to heading
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.