A Small Running Log of Hugo Theme Tweaks I Keep Repeating

Published:

Lately I’ve been back in decoration mode again—honestly, I never really stopped. A big reason is that my offline life has recently drifted into journaling, and that made me want the blog to have the same kind of cute, notebook-like feeling. I still haven’t found a theme that matches what I want closely enough, so I went back to modifying it myself.

This is mostly a cleanup note for changes I end up making over and over. I’ll probably keep adding to it later.

PS: the site has a new dark mode now. The little dot in the upper-right corner is clickable. It switches to a very cute mocha shade.

The base is still No Style Please, which I like because it gives me all the essential blog parts without piling on extra styling or animations, so it stays very fast. My part of the process is mostly borrowing bits and pieces from everywhere, asking GPT to help recreate cute styles, and then adding the functions I actually want.

Change history lives in the changelog. What follows are the practical notes.

Table of contents

April 21, 2025: I adjusted the TOC code and added a collapsible version for mobile.

1) Add layouts/partials/toc.html

    <div >
    <aside>
        {{ $emtLiPtrn := "(?s)<ul>\\s<li>\\s<ul>(.*)</li>\\s</ul>" }}
        {{ $rplcEmtLi := "<ul>$1" }}
        {{ .TableOfContents | replaceRE $emtLiPtrn $rplcEmtLi | safeHTML }}
        <!-- https://github.com/gohugoio/hugo/issues/1778#issuecomment-483880269 -->
        <!-- {{.TableOfContents}} -->
    </aside>
    <a href="#" id="toc-toggle"></a>
    </div>

2) Insert this above {{ .Content }} in \layouts\posts\single.html

    {{ if .Params.toc | default true }}
<div class="post-toc-mobile" id="post-toc-mobile" data-pagefind-ignore>
    <details>
        <summary>📑 Table of Contents</summary>
        {{ partial "toc.html" . }}
    </details>
</div>
<hr>
<div  class="post-toc" id="post-toc" data-pagefind-ignore>
    <header><strong>
        📑 Table of Contents</strong>
    </header>
    {{ partial "toc.html" . }}

3) Add this CSS to main.scss

/* 桌面端浮动 TOC */
.post-toc {
  position: fixed;
  right: 3px;
  max-width: 400px;
  overflow: auto;
  top: 100px;
  width: 12vw;
  bottom: 50px;
  font-size: 90%;
}

/* 默认隐藏移动 TOC */
.post-toc-mobile {
  display: none;
}

@media only screen and (max-width: 1224px) {
  /* 移动端显示 mobile TOC,隐藏桌面 TOC */
  .post-toc {
    display: none;
  }

  .post-toc-mobile {
    display: block;
    margin-bottom: 2rem;
    font-size: 90%;
  }

  .post-toc-mobile details {
    border: 1px solid var(--bg-color-hover);
    padding: 0.75rem;
    border-radius: 6px;
    background-color: var(--neodb-card-color);
  }

  .post-toc-mobile summary {
    font-weight: bold;
    cursor: pointer;
    font-size: 90%;
  }
}

Word count and footer stats

For Chinese word counting to work properly in Hugo, add this to config.toml:

hasCJKLanguage = true

Then place this wherever you want the word count to appear inside \layouts\posts\single.html:

{{ .WordCount }} words

I also keep a footer stats block that counts total site words and total post count, plus the number of days since the site launched. The original note I got it from seems to have disappeared, so I’m just recording the code here.

<footer class="site-footer">
    <section class="copyright">
        © 2021 -{{ if and (.Site.Params.footer.since) (ne .Site.Params.footer.since (int (now.Format "2006"))) }} {{ .Site.Params.footer.since }} - {{ end }} {{ now.Format "2006" }}
        <a href="/"> {{ $.Site.Title }} </a> · <i class="fas fa-bell"> </i>
        <a id="days">0</a> Days<br>
        {{$scratch := newScratch}}
        {{ range (where .Site.Pages "Kind" "page") }}
        {{$scratch.Add "total" .WordCount}}
        {{ end }}
        {{$var := $scratch.Get "total"}}
        {{$var = div $var 100.0}}
        {{$var = math.Ceil $var}}
        {{$var = div $var 10.0}}
        共书写了{{$var}}k字 · 共 {{ len (where .Site.RegularPages "Section" "posts") }}篇文章
    </section>

    <!-- 计算网站启动以来的天数 -->
    <script>
        var s1 = '2021-11-12'; // 设置建站日期
        s1 = new Date(s1.replace(/-/g, "/"));
        var s2 = new Date();
        var days = s2.getTime() - s1.getTime();
        var number_of_days = parseInt(days / (1000 * 60 * 60 * 24));
        document.getElementById('days').innerHTML = number_of_days;
    </script>
</footer>

Restyling the comment area

I use Waline for comments, but there are several color and border choices in its default styling that I really don’t enjoy. The annoying part is that the stylesheet is long enough to make every small adjustment feel unreasonable.

You can place the following either in the file where the comments are rendered—mine is layouts/partials/comment.html—or directly in your CSS:

<style>
    .wl-card {
        border: 0px solid #eee;
    }

    .wl-card .wl-quote {
        border-inline-start: 0px dashed rgba(237, 237, 237, .5);
    }

    .wl-card-item {
        position: relative;
        display: flex;
        padding: 0.2em;
    }

    .wl-addr {
        display: none !important;
        color: transparent !important;
    }

    .wl-content .vemoji,
    .wl-content .wl-emoji {
        display: inline-block;
        vertical-align: baseline;
        height: 3.4em !important;
        margin: -0.125em 0.25em;
    }

    img.wl-emoji,
    img.vemoji {
        vertical-align: sub;
        transition: ease-out 0.6s;
    }

    img.wl-emoji:hover,
    img.vemoji:hover {
        transform: scale(1.8) !important;
    }

    .wl-header {
        display: flex;
        overflow: hidden;
        padding: 0 4px;
        border-bottom: 0px dashed var(--waline-border-color);
        border-top-left-radius: 0em;
        border-top-right-radius: 0em;
    }

    .wl-btn.primary {
        border-color: transparent !important;
        background: Peru;
        color: white;
    }

    @media (max-width: 580px) {
        .wl-header-item:not(:last-child) {
            border-bottom: 0px dashed var(--waline-border-color);
        }
    }

    :root {
        --waline-theme-color: Peru;
        --waline-active-color: Peru;
        --waline-light-grey: unset;
        --waline-info-color: unset;
        /*--waline-color: unset;*/
        --waline-border: 0px solid peru;
    }

    .wl-editor:focus,
    .wl-input:focus {
        background: transparent;
    }

    .wl-panel {
        background-color: #fffcf9;
        /*评论区输入框的背景颜色*/
    }

    .wl-card .wl-delete,
    .wl-card .wl-like,
    .wl-card .wl-reply,
    .wl-card .wl-edit,
    .wl-card span.wl-nick,
    .wl-empty, .wl-admin-actions,
    [data-waline] p {
        color: unset;
    }

    .wl-info .wl-text-number {
        color: var(--waline-color);
        font-size: .75em;
    }

</style>

Peru is simply the theme color I picked, so you can replace it with any color value you like.

Because I didn’t have the patience to manually match every Waline text color to the theme, I unset several of them instead. In practice, that means they usually inherit the blog’s own text color, so they change along with the light and dark theme automatically. I also hid a number of borders I found especially ugly.

Previous and next post links

Hugo already supports previous/next navigation inside a section, so it’s easy to place links at the end of each post.

1) Add this below the post content in \layouts\posts\single.html

    <div class="pre-next">
        {{with .PrevInSection}}
            <a class="pre-next-btn bg" href="{{.Permalink}}"><< {{ .Title }}</a>
        {{end}}
        {{with .NextInSection}}
            <a class="pre-next-btn bg" href="{{.Permalink}}">{{ .Title }} >></a>
        {{end}}
    </div>

2) CSS

.pre-next {
  display: flex;
  text-align: center;
  justify-content: space-between;
  align-items: center;
}

.pre-next-btn {
  font-size: 1rem;
  transition: font-size 0.3s ease;
  /* 添加平滑过渡效果 */
}
/* 手机端适配:布局变为单列 */
@media (max-width: 768px) {
    .pre-next-btn {
    font-size: 3.5vw;
    }
}

Pretty convenient, honestly.

A “Museum” page

This page is a newer addition. It can work as a photo wall, a collection page, or a little display shelf for favorite items.

The actual implementation uses a shortcode. I had seen an article about building a Hugo “favorites” page and wanted something similar, but after updates there no longer seemed to be a full how-to, so I ended up piecing together my own version.

1) Add goods.html to layouts/shortcodes

<div class="goods">
    {{ range .Site.Data.goods.goods }}
    <div class="goods-bankuai">
        <img loading="lazy" class="whitey" src="{{ .image }}" alt="{{ .title }}">
        <div class="goods-info">
        <div class="goods-title">
            <div style="text-align: center;">
                <span>{{ .title }}</span>
            </div>
        </div>
        <div class="goods-note">
            {{ .description }}
        </div>
        </div>
    </div>
    {{ else }}
    <p>Nothing here...</p>
    {{ end }}
</div>

<style>
    .whitey {
        border-width: 10px;
        border-style: solid;
        border-image: url("https://i.imgur.com/8xftJ3v.gif") 7 fill round;
        width: 100%;
        height: auto;
        border-radius: 10px;
        box-sizing: border-box;
        transition: transform 0.3s ease; /* 设置过渡效果 */
    }
    .whitey:hover {
        transform: translateY(-5px); /* 鼠标悬停时,图片向上浮动 5px */
    }
    .goods {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start; /* 确保内容左对齐 */
        gap: 30px;  /* 设置照片之间的间隙 */
    }

    .goods-bankuai {
        width: calc(33.333% - 30px); /* 每张图片占父容器的三分之一宽度,减去间隙 */
        box-sizing: border-box;
        text-align: center;
        /*display: flex;*/
        flex-direction: column;
        justify-content: space-between; /* 确保每个块内容上下排列 */
        /*height: 300px; !* 固定容器高度,以避免图片撑高 *!*/
    }

    .goods-bankuai img {
        width: 100%;  /* 图片宽度为容器宽度 */
        height: 200px; /* 统一高度,确保照片高度一致 */
        object-fit: cover; /* 确保图片裁剪和填充容器 */
        border-radius: 8px; /* 可选:为图片添加圆角 */
    }

    .goods-title {
        margin-top: 0px;
    }

    .goods-note {
        margin-top: 5px;
        font-size: 0.9rem;
    }


    /* 当屏幕宽度小于 1024px 时,每行显示两张图片 */
    @media (max-width: 1024px) {
        .goods-bankuai {
            width: calc(50% - 20px); /* 每行显示两张 */
        }
    }

    /* 当屏幕宽度小于 768px 时,每行显示一张图片 */
    @media (max-width: 768px) {
        .goods-bankuai {
            width: calc(100% - 20px); /* 每行显示一张 */
            height: auto;  /* 让容器高度自适应 */
        }
        .goods-bankuai img {
            height: auto; /* 图片自适应高度,不再设置固定高度 */
        }
    }

</style>

2) Create data/goods/goods.json in the root of the blog

This stores the image and text data. Example:

[

  {
    "image": "https://pub-219f59729cc7474d97beb0f99a13e6bd.r2.dev/picture/2025/01/f8ef5b36f711097e46ef49edbe729747.png",
    "title": "凶巴巴女仆小豆泥",
    "description": "这家女仆好凶,不敢来了。"
  },
  {
    "image": "https://pub-219f59729cc7474d97beb0f99a13e6bd.r2.dev/picture/2025/01/78dbfc3707453bcbf1b47091768a0331.jpg",
    "title": "委屈擦地女仆小豆泥",
    "description": "好可怜,欺负一下,好可怜……"
  },
  {
    "image": "https://pub-219f59729cc7474d97beb0f99a13e6bd.r2.dev/picture/2025/01/635f82685c9f81e58a30fa48ad44657c.png",
    "title": "听音乐的Pingu",
    "description": "Chill, chill..."
  },
]

3) Create content/goods.md

Put the shortcode in the page body:

{/{< goods >}}

Just remember to remove the first slash when you actually use it.

Click-to-enlarge images

It seems like most people use Fancybox for image zooming, but I could never get it to behave properly on my site. In the end I gave up and went with a much more direct solution: a basic custom script.

Add this to layouts/partials/head.html

  {{/*图片放大*/}}
  <style>
    .image-row {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      justify-content: center;
      align-items: center;
    }

    .image-row img {
      flex-grow: 1;
      max-width: calc(100% / 3 - 20px);
      height: auto;
      object-fit: contain;
    }

    .image-row.two-columns img {
      max-width: calc(50% - 10px);
    }

    .image-row.three-columns img {
      max-width: calc(33.33% - 10px);
    }

    .image-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      background-color: rgba(0, 0, 0, 0.8);
      display: none;
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }

    .image-overlay.active {
      display: flex;
    }

    .image-overlay img {
      max-width: 80%;
      max-height: 80%;
    }
  </style>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      console.log('JavaScript loaded and DOMContentLoaded event fired.');

      const postContent = document.querySelector('#post-content');
      if (!postContent) {
        console.error('Error: #post-content not found!');
        return;
      }

      // 查找所有的 img 元素(无论是否在 p 标签内)
      const images = Array.from(postContent.querySelectorAll('img'));

      if (images.length === 0) {
        console.log('No images found.');
        return;
      }

      console.log(`Found ${images.length} images.`);

      // 初始化图片点击放大功能的遮罩
      const overlay = document.createElement('div');
      overlay.className = 'image-overlay';
      document.body.appendChild(overlay);

      const overlayImage = document.createElement('img');
      overlay.appendChild(overlayImage);

      overlay.addEventListener('click', () => {
        overlay.classList.remove('active');
      });

      // 处理每个段落的图片
      const paragraphs = postContent.querySelectorAll('p');

      paragraphs.forEach((paragraph, pIndex) => {
        const paragraphImages = Array.from(paragraph.querySelectorAll('img'));
        if (paragraphImages.length === 0) {
          return; // 跳过没有图片的段落
        }

        console.log(`Found ${paragraphImages.length} images in paragraph ${pIndex + 1}.`);

        if (paragraphImages.length === 1) {
          // 单张图片,保留原始布局,仅添加缩放功能
          const img = paragraphImages[0];
          const naturalWidth = img.naturalWidth || img.width;
          const naturalHeight = img.naturalHeight || img.height;

          if (naturalWidth > 200 && naturalHeight > 200) {
            img.style.cursor = 'zoom-in';
            img.addEventListener('click', () => {
              overlayImage.src = img.src;
              overlay.classList.add('active');
            });
          }
          return; // 跳过多图布局处理
        }

        // 多张图片处理,创建新布局
        const imageRow = document.createElement('div');
        imageRow.classList.add('image-row');

        // 根据图片数量调整列数
        if (paragraphImages.length === 2) {
          imageRow.classList.add('two-columns');
        } else if (paragraphImages.length === 3) {
          imageRow.classList.add('three-columns');
        } else if (paragraphImages.length === 4) {
          imageRow.classList.add('two-rows');
        } else if (paragraphImages.length === 5) {
          imageRow.classList.add('three-two-columns');
        } else if (paragraphImages.length === 6) {
          imageRow.classList.add('three-columns');
        }

        // 为每张图片添加点击放大功能
        paragraphImages.forEach((img) => {
          const naturalWidth = img.naturalWidth || img.width;
          const naturalHeight = img.naturalHeight || img.height;

          // 只为大于200x200的图片添加放大功能
          if (naturalWidth > 200 && naturalHeight > 200) {
            img.style.cursor = 'zoom-in';
            img.addEventListener('click', () => {
              overlayImage.src = img.src;
              overlay.classList.add('active');
            });
          }

          // 移动图片到新容器
          imageRow.appendChild(img);
        });

        // 替换原段落内容为图片容器
        paragraph.innerHTML = '';
        paragraph.appendChild(imageRow);
      });

      // 处理不在p标签里的图片(直接放在文章中)
      images.forEach((img, index) => {
        const naturalWidth = img.naturalWidth || img.width;
        const naturalHeight = img.naturalHeight || img.height;

        // 只为大于200x200的图片添加放大功能
        if (naturalWidth > 200 && naturalHeight > 200) {
          img.style.cursor = 'zoom-in';
          img.addEventListener('click', () => {
            overlayImage.src = img.src;
            overlay.classList.add('active');
          });
        }

        console.log(`Processing image ${index + 1}: ${img.src}, Dimensions: ${naturalWidth}x${naturalHeight}`);
      });
    });
  </script>

This script only targets images inside the section with id=post-content, and it also rearranges consecutive images into simple multi-image layouts. The downside is that it can load a bit slowly, and once in a while it even needs a refresh before it behaves properly. The upside is that it’s direct, blunt, and gets the job done.