As I wrote in introduction post - I tend to overthink. I decided to start with absolute minimum, as if against myself. So, how did I manage to design this site?
Designated goals
By saying “absolute minimum” I think of:
- using static site generator in order to be able to work with Git, not with CMS based on database and web interface
- #continuous deployment, so I can publish in automated way exactly when I want it
- minimal, though tidy and functional user interface
- multi language support (#i18n), both for content and URLs
- site available under HTTPS protocol
- no costs required (one-time or recurrent)
Decisions made
Having my tendency to overthinking in mind, I didn’t want to spend much time on research for potential solutions. I chose what I’ve already known plus brand-new things that emerged during implementation process, but I’ll cover it later.
Gitlab
For the code I chose Gitlab because:
- I use it on daily basis for years, and I know its capabilities much more than Github’s
- it allows nested project groups, which I personally really like in terms of project organisation
Cloudflare
At the beginning I wanted to use Cloudflare only for DNS management and for handling SSL certificates, but after configuring domains I saw Pages section, and I decided to try it out. I did not know this feature, even though general availability was announced year ago.
Hugo - Static Site Generator
Hugo is a static site generator, which is highly flexible, extendable and performant. I did not use it before, but I saw many technical websites built with it, so I assumed it’s right fit for content management.
Action plan
Having decided what building block I’ll use I did research how to glue them together. The plan was to create Cloudflare Pages, point custom domains there and prepare automatic deployment for content built with Hugo, using Gitlab CI.
graph LR; A[Content creation] -->|git push| B[Gitlab Pipeline] B --> C[Cloudflare Pages PL] B --> D[Cloudflare Pages EN]
It looks simple, but as it turned out later, there were complications 😅
Site skeleton
Of course at the very beginning Gitlab project and basic Hugo installation had to be created. There are many installation methods, I’ve used brew install hugo
. After creating new project using hugo new site
command, I could start working on it.
Choosing theme
I’ve decided to use Minima
, because I wanted to focus on content, not on visual effects. Feature list was good enough for my needs.
Installation of themes is done with Git submodules:
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/paper-mod
The only files that we commit to repository are .gitmodules
and directory entry, that in fact is a pointer to the submodule’s commit, which was installed (and can be later restored using git submodule init
).
In order to use theme we must configure Hugo by adding theme: paper-mod
in config/_default/config.yml
Support for i18n
I own 2 domains, so I wanted to make use of them and serve language versions separately, without using infix in URL’s path. Like in documentation I’ve created separate content/en
and content/pl
directories with proper configuration:
# config/_default/languages.yml
pl:
contentDir: content/pl
baseURL: https://blog.codito.pl/
languageName: ':poland:'
languageCode: pl
paginatePath: strona
title: Codito.pl
weight: 1
taxonomies:
category: kategorie
tag: tagi
series: serie
params:
# Must be set under languages.xx.params since Hugo 0.112 (instead of languages.xx)
# see: https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120
languageAltTitle: Polski
en:
contentDir: content/en
baseURL: https://blog.codito.dev/
languageName: ':gb:'
languageCode: en
title: Codito.dev
weight: 2
taxonomies:
category: categories
tag: tags
series: series
params:
# Must be set under languages.xx.params since Hugo 0.112 (instead of languages.xx)
# see: https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120
languageAltTitle: English
It’s not complete configuration (I’ve omitted menu definition), but it shows the concept - I wanted not only possibility of publishing in two languages, but also full language support in URLs (hence separate taxonomy definitions).
Another important feature I wanted guarantee, is ability to navigate between language versions of the same site. In chosen theme it wasn’t possible out of the box, so I had to override header.html
layout, this thread in Hugo’s community platform helped me a lot. But in order to make language switching available, site’s versions must be bound together with translationKey
which has to be defined in site’s front matter:
---
title: "Hello World!"
translationKey: "2022-04-10-hello-world"
date: 2022-04-10T01:04:46+02:00
---
Thanks to that, it’s possible to match other language versions for specific page and generate proper links.
Comment system
Minima
theme, which I chose, supports Disqus and Utteranc.es. As a reader I used both of these, and each has its own pros and cons. However, having in mind that this site is for technical content, targeting mostly people from IT, I decided to use system based on Github: utteranc.es 🙂
Integration is really simple, so I won’t describe it. There is one detail though, which I wanted to point out - permanent relationship between pages and comments. Because URL addresses can change with time, and support for multiple languages is required, again I chose translationKey
for that. Thanks to that, regardless of which language version is displayed and commented, everything will go under the same GitHub issue. We’ll see if this decision was good, but here’s how it was done, with another layout overriding:
<!-- layouts/partials/utterances.html (for Minima theme) -->
<script type="text/javascript">
const repo = '{{ .Site.Params.utterances.repo }}'
const issueTerm = '{{ if .Params.translationKey }}{{ .Params.translationKey }}{{ else }}{{ .Site.Params.utterances.issueTerm }}{{ end }}'
const theme = localStorage.theme ? `github-${localStorage.theme}` : 'preferred-color-scheme';
const script = document.createElement('script')
script.src = 'https://utteranc.es/client.js'
script.async = true
script.crossOrigin = 'anonymous'
script.setAttribute('repo', repo)
script.setAttribute('issue-term', issueTerm)
script.setAttribute('theme', theme)
script.setAttribute('label', 'comment')
document.querySelector('main').appendChild(script)
</script>
Content in sections
Another little detail is related to managing content, because I wanted to treat pages and posts differently. The former I wanted to be available under simplest URLs possible, while the latter with publish date structure.
permalinks:
posts: '/:year/:month/:slug/'
pages: '/:slug/'
Such definition ensures that regardless of how I arrange files under content/<language>/posts
, in the end they will be available under proper URL.
Worth to mention is setting removePathAccents: true
if you want to publish in language that contains special chars that don’t suit URLs well.
Automating deployment
As I said at the beginning, site is served through Cloudflare Pages. Configuration is well documented, process and interface are intuitive. What we need:
- connect Gitlab / Github account with Cloudflare
- choose repository from which site will be deployed
- set project’s name (will be later displayed on projects’ list and will be used for creating
*.pages.dev
subdomain) - configure deployment
- choose branch used for production deployment
- set build command and output directory with content to serve
- set
HUGO_VERSION
environment variable, it should be the latest one (but most important is that you should use the same version for local development)
The form allows to choose setting from template, so you can choose Hugo. In my case though, I’ve customised settings to reflect multi-language configuration, so I’ve set public/pl
as “Build output directory”. Setting hugo --verbose --log --verboseLog --debug
as build command might be useful, you’ll get more details from build process.
After approving configuration, deployment will start, and if everything have gone well, site will be available under specific subdomain. Of course, it’s better to use custom domain, especially considering easy and no-cost SSL certificate.
At this stage, each git push
to specified branch will trigger deployment process to Cloudflare Pages. It’s possible because Cloudflare creates webhook in source repository, so it gets notified about every commit activity.
Ok, we have one language version deployed, it’s time to configure second one. So again, “Create a project”, choose repository and… we get warning that repository is already in use 😩
Problems Challenges encountered
Cloudflare Pages 1:1 Gitlab Project
It turned out that Cloudflare Pages may use Github / Gitlab repository only once, so it’s still possible to serve multi-language sites, but only with language as URL path’s segment, like https://example.com/pl/
. If you want to handle it like me, with multiple domains, additional work is required.
Do you remember what I wrote about overthinking? Really, I wanted to avoid time-consuming research, so I quickly decided to choose first thing that came to my head, and I used Gitlab Pages for english version of the site 😅
I did not want to modify existing Hugo project and split it into two separate projects, so I came up with brilliant idea and in project’s .gitlab-ci.yml
I’ve added trigger for other meta-project’s pipeline:
# Trigger build for Gitlab Pages (english version of the site)
gitlab-pages:
stage: Deploy
trigger:
project: codito-net/codito-net.gitlab.io
branch: main
strategy: depend
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
Gitlab Pages domain convention
is pretty straightforward, but considering custom domain that will be used, it does not really matter how we name it. However, personally I like tidiness, so I’ve created codito-net/codito-net.gitlab.io
so Gitlab’s technical domain still is as friendly as it could be.
This project contains only Gitlab CI definition:
image: registry.gitlab.com/pages/hugo/hugo_extended:0.96.0
variables:
GIT_SUBMODULE_STRATEGY: recursive
HUGO_ENV: production
pages:
script:
- apk add --update --no-cache git go
- git clone --depth 1 --shallow-submodules https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/codito-pl/landing-page.git
- cd landing-page
- git submodule update --init --recursive
- hugo
# Workaround for: https://gitlab.com/gitlab-org/gitlab-pages/-/issues/668
- cd ..
- mkdir public
- cp -R landing-page/public/en/* public
artifacts:
paths:
- public
rules:
# See: https://docs.gitlab.com/ee/ci/triggers/index.html#configure-cicd-jobs-to-run-in-triggered-pipelines
- if: $CI_PIPELINE_SOURCE == "pipeline"
In the end Gitlab’s pipeline with continuous deployment looks like:
Let’s sum it up:
gitlab-pages
job is only a trigger that starts child pipeline in meta-project. It’s important to setstrategy: depend
in order to get this job’s status only when child pipeline ends.Cloudflare Pages
job onExternal
stage is an automatic Hugo build triggered by webhook, which will deploy polish version of the site to Cloudflare Pagespages
job onBuild
stage inDownstream
pipeline builds english version of the site and saves it as artifact, which is then automatically deployed to Gitlab Pages.
Gitlab Pages only with artifact from public
Attentive eye could see that in pages
job some workaround was used 😉 Unfortunately at this stage of work I’ve encountered problem with Gitlab’s convention (I’ve used different job’s name myself at the beginning), and also with requirement of publishing from public
directory. Problem is reported and most probably will be solved at some point and Gitlab Pages’ publishing will be possible from any directory.
Cloudflare Preview Environment baseUrl
Another challenge was related to Cloudflare Pages and its preview environments’ baseUrl
. So long as production domains are hardcoded in the configuration (inside repository), preview environments are created under random subdomain. You can’t workaround that randomness, but you can use environment aliases.
Alias is created for each non-production branch, but if you want to make use of it, you must stick to one development branch which will be used for preview deployments. In my case it’s develop
branch, so preview environment is available under develop.codito-pl.pages.dev
.
“Access policy” comes for the help, you can find it in Cloudflare Pages’ settings:
Having preview environment available under stable address, we can improve build process and set environment’s alias as baseUrl
. To be fair, I’ve stuck here for a while, because Hugo’s documentation is not clear enough here. While searching for the solution I’ve found this issue, also I’ve created community thread. In the end I was able to set base URL with HUGO_LANGUAGES_pl_baseurl
environment variable, which I’ve set for preview environments:
Final proces
After facing all of these challenges I ended with such process:
graph LR; A[Tworzenie treści] -->|git push main| B[Gitlab Pipeline] A[Tworzenie treści] -->|git push develop| G[Gitlab Pipeline] B --> C[Cloudflare External Job] B --> D[Trigger External Pipeline] C -->|Deploy PL| E[Cloudflare Pages] D -->|Deploy EN| F[Gitlab Pages] G --> H[Cloudflare External Job] H -->|Deploy Preview PL| I[Cloudflare Pages]
Probably it’s not ideal, but it does exactly what it’s supposed to. My goal was creating no-cost, two-language site with HTTPS support and automatic deployment process, and it was achieved.
At this stage there are cons:
- there is no preview environment for english version of the site
- there is no scheduled deployment, so it’s not possible to create content with
publishDate
in the future (site is built aftergit push
, so if we push such content to repository it won’t get published until anothergit push
after that date)
Rome wasn’t built in a day 😉