Setting up a free personal website with GitHub, Hugo, Netlify and TinaCMS

illustrations illustrations illustrations illustrations illustrations illustrations illustrations

Published on 2 March 2023 by Andrew Owen (8 minutes)

My first article of 2022 was on setting up a free personal website with GitHub, Hugo, Netlify and Forestry. But Forestry is due to be discontinued at the end of this month. So this is a rework of that article using Forestry’s successor TinaCMS. If you need to migrate, a tool and guide are available. Or you can do it the hard way and follow my article from earlier this year.

I spent most of last year as a developer advocate, and I felt that my personal website should be a more modern solution than WordPress. At a previous job, I’d built a developer portal with Hugo and spoken to people at Netlify and Forestry. I also already had a GitHub account. What made that a particularly attractive solution was that, aside from the domain name fees, it doesn’t cost anything. Hugo is open source. And GitHub, Netlify and TinaCMS all have a free tier for personal websites.

Next, I needed to find a portfolio style Hugo theme that would integrate with all these services. After some searching, I settled on using Kross, a free MIT-licensed theme from Themefisher. It’s built on Bootstrap, HTML5 and CSS3. I recommend finding a theme hosted on GitHub with the option to deploy to Netlify. You can fork the repository (create a copy in your own repository) and deploy it automatically. Then you just need to add TinaCMS support.

TinaCMS has come a long way since it came out of beta in November 2022. As more users adopt it, it’s become more mature. And that’s also reflected in the documentation. I was expecting to have to write a guide to setting up Hugo on Tina, but it’s already been done.

Modify your theme

You’ll have the site up and running very quickly, but you’ll want to change the placeholder text and images. I found the easiest way to replace the images was directly through the GitHub web interface. When replacing the illustration SVGs, I had to manually edit the files in VS Code to set the correct display size. For the text, save yourself the pain of YAML induced build errors and do it all in Tina Cloud.

The contact form seems to have a dependency on a non-free service, so I’ve switched it off for now and provided a mailto link instead. There also seems to be a bug in the Portfolio section where, with tags enabled, some content wasn’t rendering, so for now I’ve removed the tags.

I wasn’t completely happy with the typography, but originally the entire Kross theme is inherited as a submodule, and I was only overriding the illustrations. That way, if the theme got updated, my site would automatically get updated too. However, there were changes I needed to make and after about a week I replaced the submodule with a local copy of the theme.

With most of the static content now in place, you can concentrate on writing your blog in Tina Cloud. If you need to make developer changes to the site, you can do it locally in TinaCMS, which for a personal site means you can do without staging and deploy to your main repository in GitHub.

I had an issue with the SSL/TLS certificate. I resolved it by making the primary domain and * the redirect domain in the Netlify settings.

One tool I find invaluable when working in web applications like Tina Cloud is LanguageTool. It’s a multilingual grammar, style and spell checker. The one caveat is that it doesn’t seem to check text on TinaCMS within blockquotes. So if you use it, make sure you do the checking before applying the blockquotes style.

Check your styles

The other thing I learned post-launch is that I should’ve checked to see how the theme renders all the styles. It was doing some odd things with emphasis and code snippets, so that required some changes to the theme. Fortunately, I only had to edit the style.css file in the theme.

If you’re using the Kross theme, I’d suggest making the following changes:

ol {
list-style-type: decimal;
margin: 0

    ul {
      list-style-type: disc;
      margin: 0
    .content strong {
        font-weight: 700;
        color: #4c4c4c;
        font-size: 15px;
        line-height: 1.8;
        font-family: Roboto, sans

Otherwise, numbers and bullets are missing from lists and the bold (strong) style uses the heading font. I also decided to replace all the fonts with IBM Plex, a free alternative to Helvetica. After some user feedback, I also set the base font size to 17 pixels.

I made a change to layouts/index.html to increase the size of the social media icons:

<!-- social icon -->
<ul class="list-unstyled ml-5 mt-3 position-relative zindex-1">
{{ range }}
<li style="font-size:32px" class="mb-3"><a class="text-white" href="{{.URL | safeURL }}"><i class="{{.icon}}"></i></a></li>
{{ end }}
<!-- /social icon -->

I also removed the email / phone number / address content from the footer. I’d expect most people to contact me using one of the social media links on the front page. I’ve made the icons bigger and added Font Awesome support. Syntax highlighting is supported automatically in Hugo with Chroma, but you have to enable it. You no-longer need to use a shortcode, and TinaCMS enables you to select the tag when you enter the Markdown to start a code block.

After I got the site up and running, I made some tweaks to the theme:

  • Changing the heading font sizes.
  • Removing H3 headings from the blog entries.
  • Reducing the top background on the home page.
  • Removing the All button from the portfolio (tagging didn’t work out of the box).
  • Removing the address footer.
  • Making all the hard-coded paths relative.
  • Adding a fav icon.

RSS support

Hugo has built-in RSS support. Just add /index.xml at whatever level you want the feed to be generated from. For example: However, it won’t work if you have baseurl="/" in your config.toml file. You need to set it to your actual base URL.

There are lots of options for adding search, but I think Victoria Drake’s solution using Lunr is the best for a personal site: The only extra thing I had to do beyond the instructions was add the search form to the nav bar and style the results page to match the rest of the site.


It’s good to know who your audience is and which blog subjects attract the most interest. I recommend adding Google Analytics. After you’ve signed up for an account, all you need to do is provide your tracking ID in your config file and add a short code to the head.html partial. Or you can add it as a snippet to Netlify (I’ve found this to be the more reliable approach).


Hugo supports taxonomies out of the box. Without doing anything, I can view a tags.html page. But I hadn’t enabled tags on articles. You can add them as front matter in your TinaCMS collection (as a list). Adding tags to articles populates the tags page, but it doesn’t display the tags on the articles themselves. To do that, you need to add this code to your default single.html page:

    <div class="tags-list">
      {{- with .Params.tags -}}
        {{- if ge (len .) 1 -}}
          {{- range . -}}
            <a href="{{ $.Site.BaseURL }}tags/{{ . | urlize }}/">#{{ . }}</a>
          {{ end -}}
        {{- end -}}
      {{- end -}}

Custom 404 page

The last thing I added was a custom 404 page. After that, you’re all set.

Scheduled deploys

Netlify has recently added support for scheduled deploys. However, because I’m already using GitHub, it’s much easier to use GitHub Actions and web hooks. I found a great guide to setting it up on MC Naveen’s blog. To sumamrize, in Netlify go to Settings > Buld & Deploy > Build hooks and add a build hook. Copy the URL. Then create this GitHub action:

name: Trigger Netlify Build
    # Run at 0000 daily
    - cron: '0 0 * * *'
    name: Request Netlify Webhook
    runs-on: ubuntu-latest
      - name: Curl request
        run: curl -X POST -d {} URL_GOT_FROM_NETLIFY

You can get help with the cron syntax at Cron Helper.

Code copy button

Like others before me, I searched for a way to add a copy button to chroma code blocks in Hugo. I found Danny Guo’s original solution and others derived from it. But it wasn’t until I read this blog article that I was able to get it to work with my site. The main change I made was to use button.innerHTML instead of button.innerText to use a Font Awesome icon in place of words, thus removing the need for translation.

Image: Original by Toa Heftiba. I used one of Toa’s images that came with the Kross theme on the original article, so I wanted to highlight her work again. The previous image wasn’t exactly a forest. This isn’t exactly an alpaca.

Social shares

I adapted Ashish Lahoti’s solution for social icons. The crucial part was on how to form the URLs required by the various APIs.

Dark mode

I used Atanas Yonkov’s adaptation of Dan Abramov’s code. It works by swapping the class on the body tag. It works great, but the CSS took some time to sort out and validate with Lighthouse for accessibility. I had to drop text hinting and shadows. By default my solutino uses the operating system settings, but you can toggle any page between light and dark.

Enable search in TinaCMS

TinaCMS recently implemented search. To enable it you need to create a search token and update your config file.

Periodically, I use a broken link checker to validate internal and external links. Unfortunately, over time valid external links will break and there is an argument for linking to the Wayback Mahine version instead of the original. The downside is that you’re then not on the latest version. If only we’d got Project Xanadu instead.