Introduction

Can you believe it? Except for the domain name, the setup of my personal website is completely free! After all, this is just a site where I plan to post casual articles, so I’ll save money wherever I can. Let me explain how I built this website.

Framework Selection

Since I wanted it to be free, I couldn’t choose an SSR (Server-Side Rendering) framework like WordPress, as deploying it to a host would cost money. Although some cloud platforms offer free tiers, increased traffic would inevitably incur costs. Therefore, choosing an SSG (Static Site Generation) framework and deploying it on a platform offering free static site hosting is more sensible.

My previous website used the Gatsby framework, for a simple reason: I am very familiar with React , so I thought customization would be easier. However, I later realized I didn’t have time to maintain a bunch of Typescript and Javascript, which made me reluctant to update my website. This time, I choose Hugo . Maintaining a small amount of Go Template is easier, and I am familiar with Golang, too.

Theme Selection

Hugo’s official website offers many themes to choose from. This time, I chose the hugo-PaperMod theme. Themes usually provide download steps, just follow them.

1
2
3
4
hugo new site personal-website --format yaml
cd personal-website
git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
git submodule update --init --recursive

Configuration

You’ll have to follow the documentation provided by different themes to configure them to your liking. So in the following I only discuss some challenging configurations.

Multilingual

The theme I chose supports multiple languages, but some parts are not well translated. You’ll need to look at the source code and find a solution, or search the issues to see if anyone has provided solutions. Luckily, I found some solutions:

Last Modified Time

Similarly, search the issues for solutions:

Chinese Fonts

Themes usually don’t specifically set Chinese fonts, so Chinese characters look ugly. I often use web fonts to solve this, which is more convenient. Here, I use Google’s Noto Sans Traditional Chinese font .

First, copy the font’s embed code by clicking “Get fonts”

Get Font from Noto Sans

Then click “Get embed code”

Get embed code from Noto Sans

Then click “copy code”

Copy code from Noto Sans

Next, find where to insert HTML tags in the theme’s source code. After searching, I found this file . Create a file called layouts/partials/extend_head.html and write the copied content into it.

Then, find where the CSS controlling the fonts is located. After searching, I found this line: https://github.com/adityatelange/hugo-PaperMod/blob/9ea3bb0e1f3aa06ed7715e73b5fabb36323f7267/assets/css/core/reset.css#L27 .

Create a file called assets/css/extended/custom-font.css and write the following content:

1
2
3
4
body:lang(zh-tw) {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", "Noto Sans TC", sans-serif;
}

Note that this part is directly copied from the original code, adding the selector body:lang(zh-tw) to override the font style for Traditional Chinese. Since my articles mix Chinese and English, to avoid changing the English font, “Noto Sans TC” should be placed before the fallback font, which is before sans-serif.

Comment System

Since our site is a static site, we need to rely on external services to provide a comment system. The most famous one is Disqus . However, its free plan forces use to show a lot of ADs to the users, which I don’t like. So I chose giscus , which uses GitHub Discussions as the storage place for comments. The only downside is that it doesn’t allow users to log in and comment with Google or social media accounts like Disqus does, but since my articles are mostly technical, people reading them are likely to have a GitHub account, so it’s not a big issue.

It’s a bit tricky to support multilingual and light/dark mode switching for this comment system, so I had to write some Javascript. Here’s what it roughly looks like (sensitive information has been replaced):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<div id="giscus-script" />

<script>
  let lang = document.documentElement.lang;
  let category = "English Comments";
  let categoryId = "<your-category-id>";
  if (lang === "zh-tw") {
    lang = "zh-TW";
    category = "Traditional Chinese Comments";
    categoryId = "<your-category-id>";
  }

  let theme = localStorage.getItem("pref-theme");
  if (theme !== "light" && theme !== "dark") {
    theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light";
  }

  const giscusAttributes = {
    "src": "https://giscus.app/client.js",
    "data-repo": "<your-repo>",
    "data-repo-id": "<your-repo-id>",
    "data-category": category,
    "data-category-id": categoryId,
    "data-mapping": "pathname",
    "data-strict": "1",
    "data-reactions-enabled": "1",
    "data-emit-metadata": "0",
    "data-input-position": "top",
    "data-theme": theme,
    "data-lang": lang,
    "data-loading": "lazy",
    "crossorigin": "anonymous",
    "async": "",
  };
  const giscusScript = document.createElement("script");
  Object.entries(giscusAttributes).forEach(([key, value]) =>
    giscusScript.setAttribute(key, value)
  );
  document.getElementById("giscus-script").appendChild(giscusScript);

  function setGiscusTheme(theme) {
    if (giscusScript) {
      giscusScript.remove();
      const newGiscusScript = document.createElement("script");
      Object.entries(giscusAttributes).forEach(([key, value]) =>
        newGiscusScript.setAttribute(key, value)
      );
      newGiscusScript.setAttribute('data-theme', theme); // Set the new theme
      document.getElementById("giscus-script").appendChild(newGiscusScript);
    }
  }

  function handleStorageChange() {
    const theme = localStorage.getItem('pref-theme');
    setGiscusTheme(theme);
  }

  window.addEventListener("storage", (event) => {
    if (event.key === 'pref-theme') {
      handleStorageChange();
    }
  });

  // Override the localStorage setItem method to detect changes in the same browsing context
  const originalSetItem = localStorage.setItem;
  localStorage.setItem = function(key, value) {
    originalSetItem.apply(this, arguments);
    if (key === 'pref-theme') {
      handleStorageChange();
    }
  };
</script>

Deployment

There are many platforms for Static Site Hosting, including GitHub Pages , Netlify , Cloudflare Pages , etc. I chose Cloudflare Pages for a simple reason: its free plan has the fewest restrictions. It even has no bandwidth limit!

Deployment is straightforward. Follow the official documentation, connect to the GitHub repo, and choose hugo as the build command.

CMS

It’s inconvenient to have to sit in front of a computer and open a text editor to write a blog post every time. A common solution is to use a CMS (Content Management System). I used to use Decap CMS (formally Netlify CMS), but it wasn’t very convenient for connecting to a GitHub repo, and it didn’t generate a blank line between YAML frontmatter and the body, which sometimes caused issues. So this time, I used Tina CMS . Fortunately, Tina Cloud is free for up to two users.

Setting up Tina CMS is not difficult. Follow the official setup instructions, then go to Cloudflare Pages and change the build command to git fetch --unshallow && npx --yes tinacms build && [ "$CF_PAGES_BRANCH" = "main" ] && hugo --minify || hugo -b $CF_PAGES_URL --minify and add the environment variables. It will look like this:

Tina CMS all posts view

Tina CMS edit single post view

SEO

Favicon

A Favicon is a small icon on a webpage. Without setting it, it appears as a globe in Chrome, which doesn’t look good.

Go to Favicon Generator , upload an image, download the files, unzip them, and copy all the files into the static folder. Then refer to the theme documentation to see how to set the favicon.

OpenGraph

OpenGraph is used to set images and descriptions when your website is shared on social media. You can use https://www.opengraph.xyz/ to check how your website will look when shared on social media.

Google Search Console

Submitting your site to Google Search Console can speed up indexing. Remember to submit the sitemaps (both XML and RSS sitemaps) to speed up indexing furthermore.

Submit Sitemap to Google Search Console

LightHouse

LightHouse can be used to measure website performance and SEO. First, open the website in incognito mode to avoid interference from Chrome extensions. Then press Ctrl+Shift+J to enter Chrome Dev Tools, and select the LightHouse tab.

Open LightHouse

Select Desktop and click “Analyze page load”.

LightHouse start analyze

Results:

LightHouse Result

Note: Google Web Font Performance Tuning

When using Lighthouse, you might notice a performance issue related to Google Web Fonts. In this case, you can use the preload method to solve it:

Google Font preload performance problem

1
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:[email protected]&display=swap" rel="preload" as="font" crossorigin="anonymous">

Page Analytics

To analyze website data, such as the number of visitors, the most commonly used tool is Google Analytics , which is beyond the scope of this article, so it is not detailed here.

Conclusion

Deploying a website requires attention to many details. This article only briefly covers many topics, as different frameworks and themes require different configurations. Although you can’t directly replicate a website by reading this article, I hope it helps you notice these details when deploying your personal website.