保姆级教程——搭建Hugo博客

Sunday, January 8, 2023
本文共14983字
30分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E4%BF%9D%E5%A7%86%E7%BA%A7%E6%95%99%E7%A8%8B%E6%90%AD%E5%BB%BAhugo%E5%8D%9A%E5%AE%A2/。商业转载请联系作者获得授权,非商业转载请注明出处!

Serious sport has nothing to do with fair play. It is bound up with hatred, jealousy, boastfulness, disregard of all rules and sadistic pleasure in witnessing violence. In other words, it is war minus the shooting. — George Orwell

安装 hugo

hugo 中文文档

releases 界面下载 hugo 的 exe 文件(windows可以使用choco安装),mac 用户可以使用brew install hugo

如果要更新 hugo 的所有依赖库,使用命令go get -u -v github.com/gohugoio/hugo,注意,hugo 中文文档中给出的 GitHub 地址是已经过期的,运行后会叫你从这个地址下载(可以试试)

使用 hugo 生成站点与文章,并在本地查看

使用命令hugo new site MyBlog生成一个博客文件夹

使用命令hugo new posts\intro.md,这个命令会在本来为空的 content 文件夹下新建 posts 文件夹,并在 posts 文件夹内新建 intro.md 文件。如果你不喜欢 posts 这个名字,也可以自定义。

安装 hugo 主题,美化你的博客

hugo 有一个 themes 文件夹,使用cd themes进入该文件夹,在hugo 官方皮肤网站挑选一个喜欢的主题,使用 git 下载

这里我使用的是wangchucheng提供的Eureka主题,页面简洁美观,有些功能很完善,但是有些功能没有,本教程会在后面说明增加方法

请仔细阅读入门 - Hugo Eureka | WANG Chucheng下载安装该主题,本教程之后都是围绕该主题进行,如果你喜欢其他主题,可以查阅它的官方文档

💡请阅读完主题文档再向下阅读,以下内容基本都是进阶内容

使用 GitHub 的 pages 功能发布你的网页

这时候我们想看一下博客的效果,我们可以使用hugo -D server命令在本地查看。-D 选项默认在 public 文件夹生成页面,如果你想在 docs/文件夹生成页面,就使用hugo -d docshugo server

这时候终端会蹦出一个类似//localhost:1313/的地址,在浏览器中输入该地址就能看到本地生成的博客啦

文内图片

如果想停止本地服务器就ctrl+c即可

但是我们的博客不可能只给我们自己看啊,我们希望让互联网上的大家都看到。这时候我们有几个选择:

  • 购买自己的云服务器,价格比较昂贵,但是也有新人福利,能实现更多功能,酌情考虑
  • 使用 GitHub 的 pages 服务,作者用的就是这个
  • 使用 gitee 的 pages 服务,在国内会快一点,但是需要上传各种敏感信息,比如手持身份证照片,而且需要审核,酌情选择

注册 GitHub 账号

谷歌 GitHub 按指引注册即可

创建一个新的仓库(repository)

202209041753382022-09-04-17-53-38

仓库名最好是<你的用户名>.github.io,比如我就是 peterliu-all.github.io,如果不是这个名字可能会有一些未知的错误。

description 随便写一下,一定要设置为 pubic,一开始先不要加别的文件(不要点下面的 add readme、license 等),让仓库为空

然后复制一下仓库的 https 地址

202209041757132022-09-04-17-57-13

然后返回我们的终端,在你的博客根目录下输入hugo,这会忽略frontmatter标记draft: true,如果不想忽略草稿,就输入hugo -D

然后我们进入 public 文件夹(或者你指定的生成页面的文件夹,比如 docs),输入下列命令:

git init  <br>
git remote add origin <你复制的github给的https链接>
git add -A .
git commit -m "first commit"
git push -u origin master

然后等一会(可以进入仓库->action 查看进度)就进入我们的仓库->settings->pages 就可以看到我们的页面网址是https://<用户名>.github.io/

在它build完前查看或者build完马上查看可能会出现无样式的情况

自动build后上传脚本

将下面的命令保存为upload.ps1脚本(Windows 下的 powershell 使用)

其中的python脚本见图片上传问题

$others = $args[1..($args.Length - 1)]
If ([String]::IsNullOrWhiteSpace($args[0])) {
	## 这是作者自定义的预处理文章的python脚本
    python dispose_image.py false
}
else {
    python dispose_image.py $args[0]
}
hugo -v --log
cd public
git add -A .
If ([String]::IsNullOrWhiteSpace("$others")){
    git commit -m "$(date)"
}
else {
    git commit -m "$others"
}
git push origin master 
cd ..

其中第一个命令行参数会传递给python脚本,值为true或false,具体见图片上传问题

第二个参数就是git commit的更新信息

以后就可以使用.\upload.ps1 false "<更新message>"来上传啦

其他平台可以使用upload.py

import os
import sys
import time

if len(sys.argv) < 2:
    os.system("python dispose_image.py false")
else:
    os.system(f"python dispose_image.py {sys.argv[1]}")
os.system("hugo -v --log")
os.chdir("public")
os.system("git add -A .")
if len(sys.argv) <= 2:
    os.system(f'git commit -m "{time.strftime("%Y-%m-%dT%H:%M:%S+08:00", time.localtime())}"')
else:
    os.system(f'git commit -m "{sys.argv[2:]}"')
os.system("git push origin master")

本地渲染我也写了一个脚本,名为.\local.ps1

If ([String]::IsNullOrWhiteSpace($args[0])) {
    python dispose_image.py false
}
else {
    python dispose_image.py $args[0]
}
hugo -v --log
hugo server --disableFastRender

使用方法是.\local.ps1.\local.ps1 true来进行本地渲染了

其他平台可以使用local.py

import os
import sys

if len(sys.argv) < 2:
    os.system("python dispose_image.py false")
else:
    os.system(f"python dispose_image.py {sys.argv[1]}")

os.system("hugo -v --log")
try:
    os.system("hugo server --disableFastRender")
except KeyboardInterrupt:
    print("已退出")
except Exception as e:
    print(f"出现错误:{e}")

图片上传问题

我们可以购买阿里云等服务供应商的图床服务(OSS服务),也可以使用GitHub保存图片

Eureka主题提供了一系列的上传图片的方法,详情见文档

现在只需要知道两点:

  • 图片可以保存在/assets/images中,调用图片的时候markdown语法类似于![](/images/example.png)
  • 图片可以保存在/content/image中,调用图片的时候markdown语法类似于![](/image/example.png)

总之存放图片/附件的文件夹在build的时候会被移动到根目录

因此我们可以根据这点使用obsidian进行无缝衔接

我们将仓库设在content文件夹,设置粘贴图片时的链接为相对仓库根目录的绝对路径:

文内图片

这样粘贴example.png的时候生成的图片链接就是![](image/example.png),但是如果image前没有"/",那么这个链接会被浏览器识别为相对链接,不会指向根目录下的image文件夹,如果手动更改为![](/image/example.png),会发现obsidian和浏览器都能正确找到图片并展示,那么该怎么自动化实现这点呢?

作者的方案是使用一个python脚本自动解析并替换所有的图片链接和附件链接,将下列代码保存为dispose_image.py并对上面给的代码中的python dispose_image.py取消注释

import re
import os
import sys
from os.path import *

redispose = False
redispose = True if sys.argv[-1] == "true" else False

## 忽略的文件夹和文件名字,注意,只能识别路径中最后一个名字
ignore = ["image", "model", "archive", "stats", "search.md", "Excalidraw",
          "draw.io", "appendix"
          "Chromium OS Docs - Linux System Call Table", "subscribe"]
## image: True表示image文件夹是一个保存图片的文件夹,False表示保存附件
check_folder = {"image": True, "appendix": False}
all_pat = {folder:re.compile(f"!\[(.*)\]\({folder}(.*)\)") if jud 
           else re.compile(f"\[(.*)\]\({folder}(.*)\)") 
           for folder, jud in check_folder.items()}


## D:\database_BooksAndFiles\Blog\fnlblog\test\test.md

## 递归地处理文件夹下的内容,包括子文件夹的内容
def dispose(folder: str):
    global ignore
    dirs = os.listdir(folder)
    for file in dirs:
        if not (file.startswith(".") or file in ignore):
            print(f"[+]{file}")
            fullpath = join(abspath(folder), file)
            if isfile(fullpath) and splitext(file)[1].strip(" \n") == ".md":
                lines = []
                flag = True
                start = 0
                font_matter: int = 0
                with open(fullpath, "r", encoding="UTF-8") as f:
                    for i, line in enumerate(f.readlines()):
                        if line.strip("\n").strip() == "---":
                            if font_matter == 0:
                                font_matter = 1
                                start = i
                            elif font_matter == 1:
                                font_matter = 2
                        elif font_matter == 1 and not redispose:
                            opt = line.split(":")
                            if opt[0].strip() == "dispose" and "true" in opt[1]:
                                ## 如果frontmatter中存在dispose: true,那么就不处理该文件
                                flag = False
                                print("dispose=True,已经过处理")
                                break
                        elif font_matter == 2:
                            for fd, pat in all_pat.items():
                                for match in pat.finditer(line):
                                    line = pat.sub(
                                        f"![{match.group(1)}](/{fd}{match.group(2)})", line) \
                                        if check_folder[fd] == True \
                                        else f"[{match.group(1)}](/{fd}{match.group(2)})"
                                    print(line, end= "" if line.endswith("\n") else "\n")
                        lines.append(line)
                if flag and len(lines) > 0:
                    with open(fullpath, "w", encoding="UTF-8") as f:
                        f.writelines(lines[:start+1])
                        if not redispose:
                            ## 处理过的文件会在frontmatter中加上dispose: true
                            f.write("dispose: true\n")
                        f.writelines(lines[start+1:])
            elif isdir(fullpath):
                dispose(join(folder, file))


dispose("content")

这个脚本会解析它的命令行参数,如果传入sys.argv[1] == "true",那么它就不会管frontmatter中dispose: true项,一视同仁地对所有文件进行解析,但这可能会浪费时间,所以建议传入false或者什么都不传入,这样程序会不解析frontmatter中有dispose: true项的文件

fontawesome小插图无法显示的问题

hugo中很多时候都会用到fontswesome的小插图,但是Eureka中好像有bug无法显示

解决方法就是注册一下fontswesome并点击获取kit code代替/themes/eureka/layouts/partials/head.html中原本的链接:

文内图片

文内图片

添加评论功能

使用 disqus

Eureka中实现这点非常简单,你只需要注册disqus并获取shortname,填在params.yaml中就行: 文内图片

文内图片

相关阅读:

给Hugo添加disqus评论服务 | Marvin’s Blog【程式人生】

但是disqus在国内会被墙掉,要评论总是要使用魔法,比较麻烦,可以使用utterances

使用utterances

utterances和gittalk很像,都是使用GitHub issues的方式来保存评论

具体配置如下:

文内图片

可以去utterances查看安装方式

简单来说,需要在你的GitHub仓库内安装GitHub Apps - utterances · GitHub,然后配置好即可

文内图片

在代码旁增加复制按钮

在根目录 /static/css/ 和 /static/js/ 分别创建 copy-btn.css 和 copy-btn.js,分别写入:

.highlight {
    position: relative;
}

.highlight td:first-child {
    user-select: none;
}

.highlight pre {
    padding-right: 75px;
    /* background-color: #f8f8f8 !important; */
}

.highlight-copy-btn {
    position: absolute;
    top: 7px;
    right: 7px;
    border: 0;
    border-radius: 4px;
    padding: 1px;
    font-size: 0.8em;
    line-height: 1.8;
    /* color: #fff;
    background-color: #777;  */
    min-width: 55px;
    text-align: center;
    transition: 0.1s;
    opacity: 0.8;
}

.highlight-copy-btn:hover {
  opacity: 1;
  filter: drop-shadow(16px 16px 20px rgba(163, 163, 163, 0.178));
}
// add-copy-btn.js
(function () {
  "use strict";

  if (!document.queryCommandSupported("copy")) {
    return;
  }

  function flashCopyMessage(el, msg) {
    el.textContent = msg;
    setTimeout(function () {
      el.textContent = "Copy";
    }, 1000);
  }

  function selectText(node) {
    var selection = window.getSelection();
    var range = document.createRange();
    range.selectNodeContents(node);
    selection.removeAllRanges();
    selection.addRange(range);
    return selection;
  }

  function addCopyButton(containerEl) {
    var copyBtn = document.createElement("button");
    copyBtn.className = "highlight-copy-btn";
    copyBtn.textContent = "Copy";

    var codeEl = containerEl.firstElementChild;
    copyBtn.addEventListener("click", function () {
      try {
        var selection = selectText(codeEl);
        navigator.clipboard.writeText(selection);
        selection.removeAllRanges();
        flashCopyMessage(copyBtn, "Copied!");
      } catch (e) {
        console && console.log(e);
        flashCopyMessage(copyBtn, "Failed :(");
      }
    });

    containerEl.appendChild(copyBtn);
  }

  // Add copy button to code blocks
  var highlightBlocks = document.getElementsByClassName("highlight");
  Array.prototype.forEach.call(highlightBlocks, addCopyButton);
})();

然后在/themes/eureka/layouts/partials/footer.html末尾加上:

<link rel="stylesheet" href="{{.Site.BaseURL}}css/copy-btn.css">
<script src="{{.Site.BaseURL}}js/copy-btn.js"></script>

需要注意的是,Eureka主题还需更改config.yaml文件中markup->highlight:

markup:
  ## Do not modify markup.highlight
  highlight:
    codeFences: true
    noClasses: false
    guessSyntax : true
    lineNoStart : 1
    tabWidth : 4
    anchorLineNos : true
    lineAnchors : ''
    noHl : false

可能需要更改themes/eureka/assets/css/highlightjs.css为:

/* hljs.js */
.hljs {
  background-color: transparent !important;
  color: var(--color-tertiary-text) !important;
  padding: 0 !important;
  position: relative;
}

效果如图:

文内图片

在文章列表中显示tag

覆盖themes/eureka/layouts/partials/components/post-metadata.html为:

<div
  class="text-tertiary-text not-prose mt-2 flex flex-row flex-wrap items-center"
>
  <div class="me-6 my-2">
    <i class="fas fa-calendar me-1"></i>
    <span
      >{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span
    >
  </div>
  <div class="me-6 my-2">
    <i class="fas fa-clock me-1"></i>
    <span>{{ i18n "readingTime" .ReadingTime }}</span>
  </div>

  {{ with .GetTerms "categories" }}
    <div class="me-6 my-2">
      <i class="fas fa-folder me-1"></i>
      {{ range $index, $value := . }}
        {{ if gt $index 0 }}
          <span>, </span>
        {{ end -}}
        <a href="{{ .Permalink }}" class="hover:text-eureka"
          >{{ .LinkTitle }}</a
        >
      {{ end }}
    </div>
  {{ end }}

    {{ with .GetTerms "tags" }}
    <div class="me-6 my-2">
      <i class="fa-solid fa-tag"></i>
      {{ range $index, $value := . }}
        {{ if gt $index 0 }}
          <span>, </span>
        {{ end -}}
        <a href="{{ .Permalink }}" class="hover:text-eureka"
          >{{ .LinkTitle }}</a
        >
      {{ end }}
    </div>
  {{ end }}

  {{ with .GetTerms "series" }}
    <div class="me-6 my-2">
      <i class="fas fa-th-list me-1"></i>
      {{ range $index, $value := . }}
        {{ if gt $index 0 }}
          <span>, </span>
        {{ end -}}
        <a href="{{ .Permalink }}" class="hover:text-eureka"
          >{{ .LinkTitle }}</a
        >
      {{ end }}
    </div>
  {{ end }}
</div>

在文章开头显示字数

Eureka默认只显示阅读用时,这有时候非常蛋疼,因为机器的阅读用时和我们人差距很大,没法直观地体现出文章有多长

我们可以在themes/eureka/layouts/partials/components/post-metadata.html中阅读用时那个div前面增加:

  <div class="me-6 my-2">
    <i class="fa-solid fa-file-pen"></i>
    <span>本文共{{ plainify .WordCount }}字</span>
  </div>

也就是这里:

文内图片

使用obsidian模板自动生成文章头部

需要下载插件Templater,请阅读其官方文档中模板的写法(网上有些博客的教程是旧版的,现在已经不能用了)

示例如下:

文内图片

使用时按Alt+E就行

需要注意的是,为了避免hugo解析content文件夹中存放模板的model文件夹(模板文件夹必须存放在obsidian仓库内),我们需要在comfig.yaml中指定ignore的pattern:

文内图片

具体详见Configure Hugo |Hugo

增强内容

下面会用到的外部js代码请下载此附件

下载解压后放在/static/js中

增加归档页

请务必下载外部js代码

/themes/eureka/layouts/_default/文件夹下新建archive.html,并填入:

{{/* layouts/_default/archive.html */}}
{{ define "main" }}
{{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }}
  {{ $hasSidebar := or ($hasToc) (.Params.series) }}
  <div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12">
    <div class="col-span-2 {{ if not $hasSidebar }} {{- print " lg:col-start-2" -}} {{ end }} lg:col-span-6
      bg-secondary-bg rounded px-6 py-8">
      <!-- <h1 class="font-bold text-3xl text-primary-text">{{ .Title }}</h1> -->
      <h1 class="font-bold text-3xl text-primary-text">归档</h1>

      <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text">
        <div class="mr-6 my-2">
          <i class="fas fa-calendar mr-1"></i>
          <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span>
        </div>
      </div>

      {{ $featured := partial "utils/get-featured.html" . }}
      {{ with $featured }}
      <div class="my-4">
        {{ . }}
      </div>
      {{ end }}

      <div class="content">
        <script type='text/javascript' src="{{.Site.BaseURL}}js/jquery.min.js"></script>
        <style type="text/css">
          .car-collapse .car-yearmonth {
            cursor: s-resize;
          }
        </style>
        <script type="text/javascript">
          /* <![CDATA[ */
          jQuery(document).ready(function () {
            jQuery('.car-collapse').find('.car-monthlisting').hide();
            jQuery('.car-collapse').find('.car-monthlisting:first').show();
            jQuery('.car-collapse').find('.car-yearmonth').click(function () {
              jQuery(this).next('ul').slideToggle('fast');
            });
            jQuery('.car-collapse').find('.car-toggler').click(function () {
              if ('展开全部' == jQuery(this).text()) {
                jQuery(this).parent('.car-container').find('.car-monthlisting').show();
                jQuery(this).text('折叠全部');
              }
              else {
                jQuery(this).parent('.car-container').find('.car-monthlisting').hide();
                jQuery(this).text('展开全部');
              }
              return false;
            });
          });
            /* ]]> */
        </script>
        <div class="car-container car-collapse">
          <a href="#" class="car-toggler" style="color: darkgray;">展开全部</a>
            <ul class="car-list">
              {{ range (.Site.RegularPages.GroupByDate "2006年 01月") }}
              <li>
                <span class="car-yearmonth"><span style="color: rgba(255, 105, 105, 0.737);font-weight: bold;">{{ .Key }}</span> <span title="Post Count"> 共{{ len .Pages }}篇</span></span>
                <ul class="car-monthlisting" style="display: block;">
                  {{ range .Pages }}
                  <li style="text-indent:2em;">
                    <span style="color: rgb(100, 100, 100);">{{ .Date.Format "02日"}} </span><b><a href="{{ .Permalink }}" style="margin-left: 1em;">{{ .Title }} </a></b>
                    <!-- <span title="Comment Count">(0)</span> -->
                  </li>
                  {{ end }}
                </ul>
              </li>
              {{ end }}
            </ul>
        </div>
      </div>
    </div>
  </div>
  
  {{ end }}

然后新建content/archive/archive.md并填入frontmatter:

---
layout: archive
title: "归档"
date: 2022-12-29T3:01:44+08:00
draft: false
---

最后在menu.yaml加上对它的索引就好了:

文内图片

来源见Hugo 主题 Eureka 自定义 | 怡红院落

增加统计页

请务必下载外部js代码

/themes/eureka/layouts/_default/文件夹下新建stats.html,并填入:

{{/* layouts/_default/stats.html */}}
{{ define "main" }}

{{- $.Scratch.Add "stats" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "stats" (dict "title" .Title "slug" .Slug "year" (.Date.Format "2006") "month" (.Date.Format "2006-01") "hour" (.Date.Format "15") "week" (.Date.Format "Monday") "count" .WordCount) -}}
{{- end -}}

{{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }}
{{ $hasSidebar := or ($hasToc) (.Params.series) }}
<style>
.chart {
  margin-top: 15px;
  width: 100%;
  height: 350px;
}
@keyframes rotation{
  from {-webkit-transform: rotate(0deg);}
  to {-webkit-transform: rotate(360deg);}
}
@-webkit-keyframes rotation{
  from {-webkit-transform: rotate(0deg);}
  to {-webkit-transform: rotate(360deg);}
}
i.icon-rotate{
  transform: rotate(360deg);
  -webkit-transform: rotate(360deg);
  animation: rotation 1s linear infinite;
  -moz-animation: rotation 1s linear infinite;
  -webkit-animation: rotation 1s linear infinite;
  -o-animation: rotation 1s linear infinite;
}
</style>
<div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12">
    <div
        class="col-span-2 {{ if not $hasSidebar }} {{- print "lg:col-start-2" -}} {{ end }} lg:col-span-6 bg-secondary-bg rounded px-6 py-8">
        <!-- <h1 class="font-bold text-3xl text-primary-text">{{ .Title }}</h1> -->
        <h1 class="font-bold text-3xl text-primary-text">统计</h1>
        <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text">
          <div class="mr-6 my-2">
              <i class="fas fa-calendar mr-1"></i>
              <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span>
          </div>
        </div>
        
        {{ $featured := partial "utils/get-featured" . }}
        {{ with $featured }}
        <div class="my-4">
            {{ . }}
        </div>
        {{ end }}

        <div class="content">
          <p id="basic-info"><i class="fa-solid fa-spinner icon-rotate"></i>加载中...</p>
          <h2 id="文章数统计"></h2>
          <div id="year-stat" class="chart"></div>
          <div id="month-stat" class="chart"></div>
          <h2 id="时间段统计"></h2>
          <div id="hour-stat" class="chart"></div>
          <div id="weekday-stat" class="chart"></div>
        </div>
    </div>

    {{ if $hasSidebar}}
    <div class="col-span-2">
        {{ if .GetTerms "series" }}
        {{ partial "components/post-series.html" . }}
        {{ end }}
        {{ if $hasToc }}
        {{ partial "components/post-toc.html" . }}
        {{ end }}
    </div>
    {{ end }}
</div>




<script src="{{.Site.BaseURL}}js/echarts.min.js"></script>
<script>
const data = {{- $.Scratch.Get "stats" -}};
function showChart(id, title, type, d) {
  var chart = echarts.init(document.getElementById(id));
  var xData = [];
  var yData = [];
  d.forEach(function(item) {
      xData.push(item[0]);
      yData.push(item[1]);
  });
  var option = {
    title : { text : title },
    tooltip : { trigger : 'axis' },
    xAxis : [ { type : 'category', data : xData } ],
    yAxis : [ { type : 'value' } ],
    grid : { x : 35, y : 45, x2 : 35, y2 : 35 },
    series : [ { 
      type : 'bar',
      name : type,
      data : yData,
      markLine : {
        data : [ {
          type : 'average',
          name : '平均值'
        }],
        itemStyle : {
          normal : {
            color : '#4087bd'
          }
        }
      },
      itemStyle : {
        normal : {
          color : '#87cefa'
        }
      }
    }]
  };
  chart.setOption(option);
}

window.addEventListener('load', function() {
  basicInfo();
  yearStats();
  monthStats();
  hourStats();
  weekStats();
});
function basicInfo() {
  const articles = {{ len (where .Site.RegularPages "Section" "posts") }};
  const writeup = {{ len (where .Site.RegularPages "Section" "writeup") }};
  const principle = {{ len (where .Site.RegularPages "Section" "principle") }};
  const tricks = {{ len (where .Site.RegularPages "Section" "tricks") }};
  const pages = articles + writeup + principle + tricks;
  //const comments = data.reduce((count, article) => count + article.comments, 0);
  const words = data.reduce((count, article) => count + article.count, 0);
  document.querySelector('#basic-info').innerHTML = `
    <span>总文章数:<strong><a href="/">${pages}</a></strong> 篇</span>;
    <span>Posts:<strong><a href="/posts/">${articles}</a></strong> 篇</span>;
    <span>WriteUps:<strong><a href="/writeup/">${writeup}</a></strong> 篇</span>;
    <span>Tricks:<strong><a href="/tricks/">${tricks}</a></strong> 篇</span>;
    <span>Principle:<strong><a href="/principle/">${principle}</a></strong> 篇</span>;
    <span>总字数:<strong>${words}</strong></span>;
  `;
};

function yearStats() {
  const yearGroup = {};
  data.forEach(article => {
    const year = parseInt(article.year);
    if(!yearGroup.hasOwnProperty(year)) {
      yearGroup[year] = 0;
    }
    yearGroup[year] += 1;
  });

  const d = [];
  for(let i = 2022; i <= (new Date().getFullYear()); i++) {
    d.push([i, yearGroup[i] || 0]);
  }
  showChart('year-stat', '文章数 - 按年统计', '文章数', d);
}
function monthStats() {
  const monthGroup = {};
  data.forEach(article => {
    if(!monthGroup.hasOwnProperty(article.month)) {
      monthGroup[article.month] = 0;
    }
    monthGroup[article.month] += 1;
  });
  const d = [];
  for(let year = 2022; year <= (new Date().getFullYear()); year++) {
    for(let month = 1; month < 13; month++) {
      const text = `${year}-${month < 10 ? '0' + month : month}`;
      d.push([text, monthGroup[text] || 0]);
    }
  }
  showChart('month-stat', '文章数 - 按月统计', '文章数', d);
}
function hourStats() {
  const hourGroup = {};
  data.forEach(article => {
    const hour = parseInt(article.hour);
    if(!hourGroup.hasOwnProperty(hour)) {
      hourGroup[hour] = 0;
    }
    hourGroup[hour] += 1;
  });
  const d = [
    ['00:00-01:00'],
    ['01:00-02:00'],
    ['02:00-03:00'],
    ['03:00-04:00'],
    ['04:00-05:00'],
    ['05:00-06:00'],
    ['06:00-07:00'],
    ['07:00-08:00'],
    ['08:00-09:00'],
    ['09:00-10:00'],
    ['10:00-11:00'],
    ['11:00-12:00'],
    ['12:00-13:00'],
    ['13:00-14:00'],
    ['14:00-15:00'],
    ['15:00-16:00'],
    ['16:00-17:00'],
    ['17:00-18:00'],
    ['18:00-19:00'],
    ['19:00-20:00'],
    ['20:00-21:00'],
    ['21:00-22:00'],
    ['22:00-23:00'],
    ['23:00-24:00']
  ].map((item, key) => {
    item[1] = hourGroup[key] || 0;
    return item;
  });
  showChart('hour-stat', '文章数 - 按时段统计', '文章数', d);
}
function weekStats() {
  const weekGroup = {};
  data.forEach(article => {
    if(!weekGroup.hasOwnProperty(article.week)) {
      weekGroup[article.week] = 0;
    }
    weekGroup[article.week] += 1;
  });
  const d = [
    ['星期一', weekGroup.Monday],
    ['星期二', weekGroup.Tuesday],
    ['星期三', weekGroup.Wednesday],
    ['星期四', weekGroup.Thursday],
    ['星期五', weekGroup.Friday],
    ['星期六', weekGroup.Saturday],
    ['星期日', weekGroup.Sunday]
  ];
  showChart('weekday-stat', '文章数 - 按星期几统计', '文章数', d);
}
</script>

{{ end }}

其中函数basicInfo可自定义显示的模块信息

然后新建content/stats/stats.md并填入frontmatter:

---
layout: stats
title: "统计"
date: 2022-12-29T3:01:44+08:00
draft: false
slug: "stats"
---

来源:Hugo 主题 Eureka 自定义 | 怡红院落

增加搜索功能

请务必下载外部js代码

在config.yaml中加上:

outputs:
  home: 
    - "HTML"
    - "RSS"
    - "JSON"

然后新建themes/eureka/layouts/index.json填入:

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "summary" .Summary "permalink" .Permalink "date" (.Date.Format "2006年01月02日") "section" .Section) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

参见Hugo 之旅 | 怡红院落

themes/eureka/layouts/partials/header.html中找到有很多个div元素并列的地方,并加上:

<div class="flex">
            <div class="search-container relative pt-4 md:pt-0">
                <div class="search">
                    <form role="search" class="search-form" action="/search.html" method="get">
                    <label>
                        <input name="q" type="text" placeholder="搜索 ..." class="search-field">
                    </label>
                    <button>
                        <i class="fa-solid fa-magnifying-glass"></i>
                    </button>
                    </form>
                </div>
            </div>

然后在文件最上面加上:

<style>
.search-container {
  margin-top: -0.3rem;
  margin-right: 1rem;
}
.search-container .search {
  border: 1px solid #e2e8f0;
  border-radius: 4px;
}
.search-container input {
  padding-left: 1rem;
  line-height: 2rem;
  outline: none;
  background: transparent;
}
.search-container button {
  font-size: 0.8rem;
  margin-right: 0.5rem;
  color: #e2e8f0;
}
</style>

然后新建themes/eureka/layouts/_default/search.html,内容如下:

{{/* layouts/_default/search.html */}}
{{ define "main" }}
<div class="w-full max-w-screen-xl lg:px-4 xl:px-8 mx-auto">
  <article class="mx-6 my-8">
      <h1 id="search-count" class="font-bold text-3xl text-primary-text"></h1>
  </article>
  <div id="search-result" class="bg-secondary-bg rounded overflow-hidden px-4 divide-y"> 
    
  </div> 
</div>

<script src="{{.Site.BaseURL}}js/fuse.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script> -->
<script>
  document.addEventListener('DOMContentLoaded', async () => {
    const qs = new URLSearchParams(location.search);
    const searchResult = document.querySelector('#search-result');
    const searchCount = document.querySelector('#search-count');

    const fuseOptions = {
      shouldSort: true,
      includeMatches: true,
      threshold: 0.6,
      tokenize: true,
      location: 0,
      distance: 100,
      maxPatternLength: 32,
      minMatchCharLength: 1,
      findAllMatches: true,
      useExtendedSearch: true,
      keys: [{
          name: "title",
          weight: 0.8
        },
        {
          name: "summary",
          weight: 0.2
        },
        {
          name: "tags",
          weight: 0.6
        },
        {
          name: "catagories",
          weight: 0.2
        },
        {
          name: "date",
          weight: 0.3
        },
      ]
    };

    let fuse = null

    async function getFuse() {
      if (fuse == null) {
        const resp = await fetch('/index.json', {
          method: 'get'
        })
        const indexData = await resp.json()
        fuse = new Fuse(indexData, fuseOptions);
      }
      return fuse
    }

    function render(items) {
      console.log(items);
      return items.map(item => {
        item = item.item
        return `
<div class="px-2 py-6">
  <div class="flex flex-col-reverse lg:flex-row justify-between">
    <div class="w-full lg:w-2/3">
      <div class="my-2">
        <div class="mb-4">
          <a href="${item.permalink}" class="font-bold text-xl hover:text-eureka">${item.title}</a>
        </div>
        <div class="content">
          ${item.summary}
          <p class="more">
            <a href="${item.permalink}" title="${item.title}">阅读全文<span class="meta-nav">→</span></a>
          </p>
        </div>
      </div>
      <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text">
        <div class="mr-6 my-2" style="margin: 10px;">
          <i class="fas fa-calendar mr-1"></i>
          <span>${item.date}</span>
        </div>
        <div class="mr-6 my-2" style="margin: 10px;">
          <a href="${item.permalink}#waline-comments" title="${item.title}">
            <i class="fas fa-comment mr-1"></i>
            <span>${item.comments > 0 ? item.comments + ' 条评论' : '暂无评论'}</span>
          </a>
        </div>
        <div class="mr-6 my-2" style="margin: 10px;">
          <i class="fa-solid fa-folder-open"></i>
          <span>${item.categories}</span>
        </div>
        <div class="mr-6 my-2" style="margin: 10px;">
          <i class="fa-solid fa-tags"></i>
          <span><b>${item.tags}</b></span>
        </div>
        
      </div>  
    </div>
    <div class="w-full lg:w-1/3 mb-4 lg:mb-0 lg:ml-8">
      ${item.featuredImage ? `<img src="${item.featuredImage}" class="w-full" alt="Featured Image">` : ''}
    </div>
  </div>
</div>`;
      }).join('');
    }

    function updateDOM(html, keyword, number) {
      document.title = document.title.replace(/包含关键词.*?文章/, `包含关键词 ${keyword} 的文章`)
      searchResult.innerHTML = html
      searchCount.innerHTML = `共查询到 ${number} 篇文章`
    }

    async function search(searchString) {
      console.log(searchString);
      let result = [];
      if(searchString) {
        const fuse = await getFuse()
        result = fuse.search(searchString)
      }
      const html = render(result)
      updateDOM(html, searchString, result.length)
    }

    document.querySelectorAll('input[name="q"]').forEach(el => el.value = qs.get('q'));
    search(qs.get('q') || '')

    window.blogSearch = function(keyword) {
      if(!keyword) {
        return;
      }

      history.pushState('', '', location.pathname + '?q=' + encodeURIComponent(keyword));
      document.querySelectorAll('input[name="q"]').forEach(el => el.value = keyword);
      search(keyword);
    }
  })
</script>
{{ end }}

然后新建content/search.md填入:

---
title: "搜索结果"
sitemap:
  priority : 0.1
layout: "search"
date: "2022-12-29T05:02:20+08:00"
slug: "search.html"
---

来源:Hugo 主题 Eureka 自定义 | 怡红院落

自定义RSS文件

请务必下载外部js代码

新建themes/eureka/layouts/_default/rss.xml,然后可以填入go模板,下面是一个示例:

{{- $page_context := cond .IsHome site . -}}
{{- $pages := $page_context.RegularPages -}}
{{- $limit := site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
  {{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ if ne .Title site.Title }}{{ with .Title }}{{.}} | {{ end }}{{end}}{{ site.Title }}</title>
    <link>{{ .Permalink }}</link>
    {{- with .OutputFormats.Get "RSS" }}
      {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
    {{ end -}}
    <description>{{ .Title | default site.Title }}</description>
    <generator>Hugo -- gohugo.io AND <a href="https://github.com/wangchucheng/hugo-eureka/">Eureka</a></generator>
    {{- with site.LanguageCode }}<language>{{.}}</language>{{end -}}
    {{- with site.Copyright }}<copyright>{{ replace (replace . "{year}" now.Year) "&copy;" "©" | plainify }}</copyright>{{end -}}
    {{- if not .Date.IsZero }}<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end -}}
    {{ range $pages }}
    <item>
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      <guid>{{ .Permalink }}</guid>
      {{ with .Description }}
      <description>{{ . }}</description>
      {{ else }}
      {{if .IsPage}}
      <description>{{ .Summary }}</description>
      {{ else }}
      {{ with .Site.Params.Description }}
      <description>{{ . }}</description>
      {{ end }}
      {{ end }}
      {{ end }}
    </item>
    {{ end }}
  </channel>
</rss>

增加订阅功能

请务必下载外部js代码

首先,此功能要求网站有一个RSS文件,一般会自动生成为index.xml

法一:TinyLetter+Zapier

使用TinyLetter的获取订阅者服务+Zapier的自动发信服务,具体步骤详见Newsletter for Hugo Blog - Backendology - A study of backend web development by Jared Ririe

注意,TinyLetter是免费的,但是Zapier似乎是付费的(有试用期)

不建议使用TinyLetter,功能简陋,且搞自动化发邮件容易被系统ban掉

法二:使用follow.it

我们还可以使用更自动化、更便捷的follow.it | Feedburner Alternative - Get more readers,有免费计划也有付费计划,付费计划价格也较低

直接根据官网的指引走就好了,然后它就会自动解析你的RSS页面(前提是你要有RSS)中最近发表的文章,并将24小时内更新的文章以邮件形式发送给订阅者,同时它还有一个类似于展览一样的东西,读者可以直接从它的网站发现你的网站并进行订阅。

follow.it分两种发信方式,一种是NewsPaper,在每天的固定时间向订阅者推送消息或邮件;另一种是single email,会在检测到新文章后自动生成邮件发送

follow.it还自带RSS阅读器,这样我们的RSS页面可以变的更美观,但是不建议用这个页面代替我们的RSS页面,因为我们的RSS页面的信息可能被别的网站采集并获取最新消息,所以不建议换掉这个页面

follow.it的缺点是有时候对RSS的变化响应不够迅速,还有免费版功能比较简陋

在博客内部某个地方添加订阅入口

记得在自己的博客内部增加一个进入订阅页面的入口,可以把订阅页面(https://tinyletter.com/<你的名字>)下载下来(可以自定义样式),放在themes/eureka/layouts/_default并重命名为subscribe.html,然后新建/content/subscribe/subscribe.md并填入frontmatter:

---
layout: subscribe
title: "订阅"
date: 2023-01-07T17:06:54+08:00
draft: false
slug: "subscribe"
---

然后在about页加一个小按钮:

文内图片

具体代码示例如下:

<style>
  .button {
            font-family: 宋体,'Comic Sans MS', cursive;
            color: #FFFFFF;
            background-color: #333333;
            display: inline-block;
            white-space: nowrap;
            height: 40px;
            line-height: 42px;
            margin: 0 5px 0 0;
            padding: 0 22px;
            text-decoration: none;
            /* text-transform: uppercase; */
            text-align: center;
            font-weight: medium;
            font-style: normal;
            font-size: 14px;
            cursor: pointer;
            border: 0;
            -moz-border-radius: 4px;
            border-radius: 4px;
            -webkit-border-radius: 4px;
            vertical-align: top;
            -webkit-font-smoothing: antialiased;
            font-smoothing: antialiased;
        }
</style>
<a href="/subscribe/"><button class="button">立即订阅个人博客</button></a>    

还可以在各种地方添加一个小铃铛图片,这样用户更容易找到你的订阅入口,而不用返回到主页

让你的博客能被google等搜索引擎找到

google可以使用Google Search Console,详见让Google搜索到自己的博客

bing可以使用Bing Webmaster Tools,详见如何讓你的 WordPress 網站能被 Bing 索引? | 小翔教你SEO,注意bing是可以导入google search console的数据的,因此建议先设置google

百度同理,使用普通收录_加快网站内容抓取,快速提交数据工具_站长工具_网站支持_百度搜索资源平台

更多阅读:

使黑色图片在深色模式下反转为白色图片

下面提供一个示例。

比如我在about.md中定义的头像是黑色透明背景的,在深色模式下就完全看不清图片:

文内图片

我们可以在themes/eureka/layouts/partials/widgets/about.html文件头部加上:

<style>
  .dark .avatar{
    filter: invert(100%);
  }
</style>

然后给alt="Avatar"的img标签加上avatar类:

文内图片

效果:

文内图片

文内图片

让深浅颜色变换更平滑

在themes/eureka/layouts/partials/head.html中增加:

<style>
  .dark{
    transition: 1.5s;
  }
  .light{
      transition: 1.5s;
  }
</style>

文内图片

然后因为eureka主题实现深浅变化是根据有没有dark类来区分的,所以没有light类,因此我们需要更改themes/eureka/layouts/_default/baseof.html中的html标签:

<html
  lang="{{ .Site.LanguageCode }}"
  dir="{{ .Site.Language.LanguageDirection | default "ltr" }}"
  {{ if eq .Site.Params.colorScheme "dark" }}class="dark"
  {{ else }} class="light"
  {{ end }}
>

文内图片

这样在深浅主题变化的时候,颜色过渡就会平滑一点了,具体成果见我的博客

增加回到开头按钮

themes\eureka\layouts\partials\components\post-article.html末尾增加:

<style>
.black-circle{
    height: 45px;
    width: 45px;
    display: block;
    font-size: 30px;
    transition: all 1.5s;
    position:fixed;
    right:0px;
    bottom:0px;
    z-index: 100;
    background: transparent;
}
.black-circle:hover{
    transform: translateY(-10px);
}
</style>
<script>
function goTop(acceleration, time) {
    acceleration = acceleration || 0.1;
    time = time || 16;

    var x1 = 0;
    var y1 = 0;
    var x2 = 0;
    var y2 = 0;
    var x3 = 0;
    var y3 = 0;

    if (document.documentElement) {
        x1 = document.documentElement.scrollLeft || 0;
        y1 = document.documentElement.scrollTop || 0;
    }
    if (document.body) {
        x2 = document.body.scrollLeft || 0;
        y2 = document.body.scrollTop || 0;
    }
    var x3 = window.scrollX || 0;
    var y3 = window.scrollY || 0;

    // 滚动条到页面顶部的水平距离
    var x = Math.max(x1, Math.max(x2, x3));
    // 滚动条到页面顶部的垂直距离
    var y = Math.max(y1, Math.max(y2, y3));

    // 滚动距离 = 目前距离 / 速度, 因为距离原来越小, 速度是大于 1 的数, 所以滚动距离会越来越小
    var speed = 1 + acceleration;
    window.scrollTo(Math.floor(x / speed), Math.floor(y / speed));

    // 如果距离不为零, 继续调用迭代本函数
    if(x > 0 || y > 0) {
        var invokeFunction = goTop( acceleration , time );
        window.setTimeout(invokeFunction, time);
    }
}
</script>
<a><i id="return-top" class="fa-solid fa-circle-up black-circle" onclick="goTop(0.1, 16)"></i></a>

感谢关于top按钮的网页设置 - zhunaoke - 博客园中提供的代码

具体效果:

文内图片

增加图片放大查看功能

本部分灵感来自于Hugo 使用 Fancybox 实现图片灯箱/放大功能 - atpX,其作者灵感来自于maupassant-hugo/render-image.html at master · flysnow-org/maupassant-hugo · GitHub,但是该文作者的方法似乎有误,我更改了以后在我的页面上终于可以使用了

在params.yaml添加:

fancybox : true

或者在config.yaml添加:

params:
  fancybox : true

然后新建themes/eureka/layouts/_default/_markup/render-image.html(包括文件夹),添加代码:

{{- $img_destination := .Destination -}}
{{- if (and .Page.Site.Params.image_cdn.enable (not .Page.Site.IsServer)) -}}
    {{if hasPrefix .Destination "/" }}
    {{ $img_destination = (print .Page.Site.Params.image_cdn.HOST .Destination) }}
    {{ else if not (hasPrefix .Destination "http") }}
    {{ $img_destination = (print .Page.Site.Params.image_cdn.HOST (path.Join .Page.RelPermalink .Destination)) }}
    {{ end }}
{{- end -}}

{{- if .Title -}}
<figure class="max-w-2xl mx-auto overflow-hidden">
    {{if .Page.Site.Params.fancybox }}
    <a data-fancybox="gallery" href="{{ $img_destination | safeURL }}">
        <img alt="{{ $.Text }}" src="{{ $img_destination | safeURL }}" />
    </a>
    {{ else }}
        <img alt="{{ $.Text }}" src="{{ $img_destination | safeURL }}" />
    {{ end }}
    <figcaption class="p-2 text-center">{{ with $.Title | safeHTML }}{{ . }}{{ end }}</figcaption>
</figure>
{{- else -}}
    {{if .Page.Site.Params.fancybox }}
        <a data-fancybox="gallery" href="{{ $img_destination | safeURL }}">
            <img class="mx-auto" alt="{{ $.Text }}" src="{{ $img_destination | safeURL }}" />
        </a>
    {{ else }}
        <img class="mx-auto" alt="{{ $.Text }}" src="{{ $img_destination | safeURL }}" />   
    {{ end }}
{{- end -}}

✍️ps.该代码与maupassant-hugo/render-image.html at master · flysnow-org/maupassant-hugo · GitHub的代码一致

在themes/eureka/layouts/partials/custom-head.html增加(也可以在同目录下的head.html或footer.html增加)以下代码:

{{if .Page.Site.Params.fancybox }}
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css" />
<script src="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js"></script>
{{ end }}

以后如果不想要图片放大功能了,只用更改fancybox的值就可以了

在文章开头增加版权声明

我们希望在自己原创的文章开头放一个版权声明,但是在自己转载的文章开头又不放,我们可以在themes/eureka/layouts/partials/components/post-article.html内的{{ .Content }}上方加上:

{{ if (or  (not .Params.reprint) ( not ( in "true" .Params.reprint )) )  }}
  <b><p>⚠️本文是<a href="https://github.com/PeterLiu-all">作者P3troL1er</a>原创,首发于<a href="{{ .Permalink }}">{{ .Site.BaseURL }}</a>。商业转载请联系作者获得授权,非商业转载请注明出处!</p></b>
{{ end }}

复制内容自动增加版权信息

我们希望所有复制的文字和html都在末尾附上版权信息还有自己网站的链接,但是按copy按钮复制的代码却不希望加上信息(这样会很麻烦),于是我更改了复制网页内容自动添加版权信息的方法_idjl的博客-CSDN博客的代码,在themes/eureka/layouts/partials/custom-head.html末尾加上:

<script language="javascript">
    function setClipboardText(event) {
        event.preventDefault();//阻止元素发生默认的行为(例如,当点击提交按钮时阻止对表单的提交)。
        var node = document.createElement('div');
        //对documentfragment不熟,不知道怎么获取里面的内容,用了一个比较笨的方式
        node.appendChild(window.getSelection().getRangeAt(0).cloneContents());
        //getRangeAt(0)返回对基于零的数字索引与传递参数匹配的选择对象中的范围的引用。对于连续选择,参数应为零。
        var htmlData =
            "<div>" +
            node.innerHTML +
            "<br /><br />著作权归作者P3troL1er所有。<br />" +
            "商业转载请联系作者P3troL1er获得授权,非商业转载请注明出处。<br />" +
            '作者:P3troL1er<br />链接:<a href="https://peterliuzhi.top/">https://peterliuzhi.top/</a><br />' +
            "</div>";
        var textData = ""
        if (window.getSelection().anchorNode.parentElement.className == "highlight")
            textData =
                window.getSelection().getRangeAt(0)
        else
            textData =
                window.getSelection().getRangeAt(0) +
                "\n\n著作权归作者所有。\n" +
                "商业转载请联系作者获得授权,非商业转载请注明出处。\n" +
                "作者:P3troL1er\n链接:https://peterliuzhi.top/\n";
        if (event.clipboardData) {
            event.clipboardData.setData("text/html", htmlData);
            //setData(剪贴板格式, 数据) 给剪贴板赋予指定格式的数据。返回 true 表示操作成功。
            event.clipboardData.setData("text/plain", textData);
        } else if (window.clipboardData) {
            //window.clipboardData的作用是在页面上将需要的东西复制到剪贴板上,提供了对于预定义的剪贴板格式的访问,以便在编辑操作中使用。
            return window.clipboardData.setData("text", textData);
        }
    }
    document.addEventListener("copy", function (e) {
        setClipboardText(e);
    });
</script>

增加友链页面

新建layouts/shortcodes/friend.html(包括文件夹),写入:

{{- if .IsNamedParams -}}
<a target="_blank" href={{ .Get "url" }} title={{ .Get "name" }} class="friendurl">
  <div class="frienddiv">
    <div class="frienddivleft">
      <img class="myfriend" src={{ .Get "logo" }} />
    </div>
    <div class="frienddivright">
        <b>
        <div class="friendname">{{- .Get "name" -}}</div>
        <div class="friendinfo">{{- .Get "word" -}}</div>
        </b>
    </div>
  </div>
</a>
{{- end }}

然后新建content/friend/links.md写入:

---
title: Links
reprint: true
date: 2023-01-19T21:51:25+08:00
---
<style>
.friendurl {
    text-decoration: none !important;
    color: black;
    box-shadow: none !important;
}

.myfriend {
    width: 56px !important;
    height: 56px !important;
    border-radius: 50%!important;
    padding: 2px;
    margin-top: 20px !important;
    margin-left: 14px !important;
    background-color: transparent;
}

.frienddiv {
    overflow: auto;
    height: 100px;
    width: 49%;
    display: inline-block !important;
    border-radius: 5px;
    border-width: 0 !important;
    border-color: transparent !important;
    -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
    -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
    box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
    background: none;
    overflow:hidden;
    -webkit-transition: all ease-out 1s;
    -moz-transition: all ease-out 1s;
    -o-transition: all ease-out 1s;
    transition: all ease-out 1s;
}

.dark .frienddiv{
    -moz-box-shadow: 0 1px 6px rgba(255, 255, 255, 0.12) !important;
    -webkit-box-shadow: 0 1px 6px rgba(255, 255, 255, 0.12) !important;
    box-shadow: 0 1px 6px rgba(255, 255, 255, 0.12) !important;
}

.dark .frienddiv:hover {
    background: var(--code-bg);
}

.frienddiv:hover {
    background: var(--theme);
    transition: transform 1s;
    webkit-transform: scale(1.1);
    -moz-transform: scale(1.2);
    -ms-transform: scale(1.2);
    -o-transform: scale(1.2);
    transform: scale(1.1);
}

.frienddiv:hover .frienddivleft img { 
    transition: 0.9s !important;
    -webkit-transition: 0.9s !important;
    -moz-transition: 0.9s !important;
    -o-transition: 0.9s !important;
    -ms-transition: 0.9s !important;
    transform: rotate(360deg) !important;
    -webkit-transform: rotate(360deg) !important;
    -moz-transform: rotate(360deg) !important;
    -o-transform: rotate(360deg) !important;
    -ms-transform: rotate(360deg) !important;
}

.frienddivleft {
    width: 92px;
    float: left;
    margin-right: -5px;
}

.frienddivright {
    margin-top: 18px;
    margin-right: 18px;
}

.friendname {
    text-overflow: ellipsis;
}

.dark .friendname,.dark .friendinfo{
    color: white;
}

.friendinfo {
    text-overflow: ellipsis;
}

@media screen and (max-width: 600px) {
    .friendinfo {
        display: none;
    }
    .frienddivleft {
        width: 84px;
        margin: auto;
    }
    .frienddivright {
        height: 100%;
        margin: auto;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .friendname {
        font-size: 18px;
    }
}
</style>
<!-- 记得把"\"删掉,因为hugo的shortcode的原因不能直接打两个{ -->
\{\{< friend name="P3troL1er的博客" url="https://peterliuzhi.top/" logo="https://peterliuzhi.top/images/avatar.png" word="P3troL1er的博客" >\}\}
<br><hr>

顺便一提,记得给友链页面设置入口,可以在menus.yaml设置:

main:
  - name: Friends
    url: /friend/links/
    weight: 6

感谢Hugo博客添加友链 - 腾讯云开发者社区-腾讯云中提供的代码

添加博客生存时间计数

最近看到hexo博客的一个主题有一个很棒的功能,仔细研究后发现是纯由js实现的,就打算给它嫖过来(

文内图片

我们在themes/eureka/layouts/partials/footer.html中原本的底部记录信息下面添加一句html

文内图片

添加:

<p class="text-sm text-tertiary-text">本博客有<span id="since" style="color: var(--color-eureka);">0</span>天的历史</p>

然后在这个元素的下面(注意,位置要在它下面)添加一个script:

<script>
function show_date_time () {
		// 填入你的博客的诞生日期,格式为 月/日/年 时/分/秒
        var BirthDay = new Date("10/26/2022 0:00:00");
        var today = new Date();
        var timeold = (today.getTime() - BirthDay.getTime());
        var msPerDay = 24 * 60 * 60 * 1000
        var day = Math.floor(timeold / msPerDay)
        since.innerHTML = day
    }
    show_date_time()
</script>

改一下你的博客的诞生日期就可以啦,这个脚本会根据用户打开网页的时间实时更新生存时间,效果如下:

文内图片

使文章的导航栏可滚动

因为Eureka的文章导航栏是不可滚动的,要等到文章读到那一部分才能滚动到那里,非常不方便

要使它能够优雅的滚动(让难看的滚动条不可见),可以在themes/eureka/layouts/partials/head.html加上一段CSS代码:

<style>
div#nav{
  position: fixed;
  padding-bottom: 5vh;
  padding-right: 2vh;
  height:70vh;
  top: 20vh;
  width: fit-content;
  overflow-y: auto;
  -webkit-transition: all 1s cubic-bezier(0.02, 0.01, 0.47, 1);
  transition: all 1s cubic-bezier(.02, .01, .47, 1);
}
div#nav:hover{
  box-shadow: 0 0px 32px 0 rgba(48, 55, 66, 0.073);
}
div#nav::-webkit-scrollbar {
  display: none !important;
}
</style>

然后更改themes/eureka/layouts/partials/components/post-toc.html为:

<div
  class="sticky-toc"
  id="nav"
>
<b><h2 style="font-size: larger;">{{ i18n "onThisPage" }}</h2></b>
  {{ .TableOfContents }}
</div>

效果如下:

文内图片

增加分享二维码和邀请信息

效果如下:

文内图片

themes/eureka/layouts/partials/components/post-article.html</article>前面加上下面的代码:

<script type="text/javascript">
//显示图片
function displayImg(trans) {
    var img = document.getElementById("qrcode");

    var x = event.clientX + document.body.scrollLeft + 20 - trans;
    var y = event.clientY + document.body.scrollTop - 5 - trans; 

    img.style.left = x + "px";
    img.style.top = y + "px";
    img.style.display = "block";
}

//图片消失
function vanishImg(){
    var img = document.getElementById("qrcode");
    img.style.display = "none";
}
function flashCopyMessage(el, msg) {
    el.textContent = msg;
    setTimeout(function () {
      el.textContent = "点此复制分享信息!";
    }, 1000);
  }
  function basic_copy(){
    let message = "在吗?有篇博文写的挺好的,标题是 {{ .Title }} ,值得一读👍\n详情点击{{ .Permalink }}\n" + "\n\n著作权归作者所有。\n" +
      "商业转载请联系作者获得授权,非商业转载请注明出处。\n" +
      "作者:P3troL1er\n链接:https://peterliuzhi.top/\n";
    navigator.clipboard.writeText(message);
  }

  function copy_invite_message(){
    const msgbtn = document.querySelector("#copy_invite_msg");
    let message = "在吗?有篇博文写的挺好的,标题是 {{ .Title }} ,值得一读👍\n详情点击{{ .Permalink }}\n" + "\n\n著作权归作者所有。\n" +
    "商业转载请联系作者获得授权,非商业转载请注明出处。\n" +
    "作者:P3troL1er\n链接:https://peterliuzhi.top/\n";
    navigator.clipboard.writeText(message)
        .then(
          () => {
            flashCopyMessage(msgbtn, "已复制分享信息!");
            console.log("Copied to clipboard successfully!");
          },
          () => {
            flashCopyMessage(msgbtn, "复制分享信息失败:(");
            console.error("Unable to write to clipboard.");
          }
        );
}
</script>
<button id="copy_qrcode" class="button" onmouseover="displayImg(0);" onmouseout="vanishImg()" onmousemove="displayImg(0);">点此复制分享二维码!</button> 

<button id="copy_invite_msg" class="button" onclick="copy_invite_message();">点此复制分享信息!</button>

<a><i id="invite_icon" class="fa-solid fa-share invite_icon" onmouseover="displayImg(330);" onmouseout="vanishImg()" onmousemove="displayImg(330);" onclick="basic_copy();"></i></a>

<a href="/subscribe/"><i class="fa-solid fa-bell ld sub_icon"></i></a>

<div id="qrcode" style="width: 300px;height: 300px;display:none;position: fixed;">
  <img src="https://api.qrserver.com/v1/create-qr-code?data={{ .Permalink }}&size=300x300" alt="生成二维码失败!检查检查网络?" style="background-color: rgba(255, 255, 255, 0.904); margin:0;" id="qrcode_img">
  <div style="width: 300px;height: fit-content; background-color: rgba(255, 255, 255, 0.582);">
    <h3 style="text-align: center; margin:0;">扫码阅读此文章 <br /> 点击按钮复制分享信息</h2>
  </div>
</div>

<script>
const testImg = document.querySelector("#qrcode_img");
const btn = document.querySelector("#copy_qrcode");
function flashCopyMessage(el, msg) {
    el.textContent = msg;
    setTimeout(function () {
      el.textContent = "点此复制分享二维码!";
    }, 1000);
  }
function handleCopyImg() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'rgba(255, 255, 255, 0)';
  const img = new Image();

  canvas.width = testImg.width;
  canvas.height = testImg.height;
  img.crossOrigin = "Anonymous";
  img.src = testImg.src;
  
  img.onload = () => {
    ctx.clearRect(0, 0, testImg.width, testImg.height);
    ctx.drawImage(img, 0, 0);
    // 将canvas转为blob
    canvas.toBlob(async blob => {
      console.log(blob);
      const data = [
        new ClipboardItem({
          [blob.type]: blob,
        }),
      ];
      // https://w3c.github.io/clipboard-apis/#dom-clipboard-write
      await navigator.clipboard.write(data)
        .then(
          () => {
            flashCopyMessage(btn, "已复制分享二维码!");
            console.log("Copied to clipboard successfully!");
          },
          () => {
            flashCopyMessage(btn, "复制分享二维码失败:(");
            console.error("Unable to write to clipboard.");
          }
        );
      });
  }
}

btn.addEventListener("click", handleCopyImg, false);
//msgbtn.addEventListener("click", copy_invite_message, false);
</script>

<br />

其中buttun的样式是在增加订阅功能里订阅按钮使用的(增加了一些样式),如下:

<style>
  .button {
            font-family: 宋体,'Comic Sans MS', cursive;
            color: #FFFFFF;
            background-color: #333333;
            display: inline-block;
            white-space: nowrap;
            height: 40px;
            min-width: 230px;
            line-height: 42px;
            margin: 0 5px 0 0;
            padding: 0 22px;
            text-decoration: none;
            /* text-transform: uppercase; */
            text-align: center;
            font-weight: medium;
            font-style: normal;
            font-size: 14px;
            cursor: pointer;
            border: 0;
            -moz-border-radius: 4px;
            border-radius: 4px;
            -webkit-border-radius: 4px;
            vertical-align: top;
            -webkit-font-smoothing: antialiased;
            font-smoothing: antialiased;
        }
    .button:hover {
        cursor: pointer;
        animation: jelly 0.7s;
    }
 
    @keyframes jelly {
 
        0%,
        100% {
            transform: scale(0.1, 0.1);
        }
 
        33% {
            transform: scale(0.05, 0.15);
        }
 
        66% {
            transform: scale(0.15, 0.05);
        }
    }
 
    @keyframes jelly {
 
        0%,
        100% {
            transform: scale(1, 1);
        }
 
        25%,
        75% {
            transform: scale(0.9, 1.1);
        }
 
        50% {
            transform: scale(1.1, 0.9);
        }
    }
    i.invite_icon{
      height: 5vh;
      width: 5vh;
      display: block;
      font-size: 30px;
      transition: all 1.5s;
      position: fixed;
      right: 1vh;
      bottom: 7vh;
      z-index: 100;
      background: transparent;
      transition: 0.1s;
  }
  i.invite_icon:active{
    transform:rotate(-45deg);
  }

  i.sub_icon{
    height: 5vh;
    width: 5vh;
    display: block;
    font-size: 30px;
    transition: all 1.5s;
    position: fixed;
    right: 1vh;
    bottom: 13vh;
    z-index: 100;
    background: transparent;
}
</style>

使用sweetalert改进弹窗消息

查看SweetAlert官方文档后,在themes/eureka/layouts/partials/head.html处加上:

<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>

读者打开网站主页自动推送最新文章消息

themes/eureka/layouts/partials/footer.html末尾加上:

<script>
    function is_weixn(){
      var ua = navigator.userAgent.toLowerCase();
      if(ua.match(/MicroMessenger/i)=="micromessenger") {
          return true;
      } else {
          return false;
      }
  }
  if(is_weixn()){
    alert("检测到您使用的浏览器是微信内置浏览器,渲染会出现严重问题,极度影响阅读体验,建议使用其他浏览器打开本网址。\n请复制本网址到剪切板,然后前往手机浏览器内打开:\n{{ .Permalink }}(或者右上角用浏览器打开)");
  }else{
    window.alert = function(msg1, msg2, msg3){
        swal(msg1+"", msg2+"", msg3+"");
      }
   {{- $page_context := cond .IsHome site . -}}
  {{- $pages := $page_context.RegularPages -}}
  {{- $pages = $pages | first 1 -}}
  {{ if eq .Permalink .Site.BaseURL }}
    {{ range $pages }}
        swal({
            title: '{{ .Date.Format "2006年01月02日" | safeHTML }}\n最新发布博文:\n',
            text: decodeURIComponent("{{ .Title }}\t{{ .Permalink }}"),
            icon: "info",
            buttons: {
                copy: {
                    text: "复制链接",
                    value: "copy",
                    className: "grey-bg"
                },
                direct: {
                    text: "阅读博文",
                    value: "direct"
                }
                ,
                out: {
                    text: "我知道了",
                    value: "cancel",
                    className: "red-bg"
                }
            },
            dangerMode: false
          }).then((value) => {
            switch (value) {
 
                case "copy":
                  navigator.clipboard.writeText("{{ .Permalink }}");
                  swal("复制成功!", "您可以粘贴到浏览器地址栏查看博文", "success");
                  break;
             
                case "direct":
                  window.open("{{ .Permalink }}", "{{ .Title }}");
                  break;
             
                case out:
                default:
                  break;
              }
            });
    {{ end }}
  {{ end }}
  }
  
  </script>

代码里还包括了检测是否是微信内置浏览器,如是,弹出警告信息

然后根据需要定义button的样式(更改代码中的className字段),下面是我定义的class,仅供参考:

.red-bg{
    background-color: rgba(254, 3, 3, 0.667);
  }
  .red-bg:hover{
    background-color: rgba(254, 3, 3, 0.769) !important;
  }
  .grey-bg{
    background-color: rgba(130, 124, 124, 0.758);
  }
  .grey-bg:hover{
    background-color: rgba(130, 124, 124, 0.888) !important;
  }

然后对深色模式进行兼容(在head.html加上):

<style>
.dark .swal-modal{
    filter: invert(80%);
    
  }
  .dark .swal-overlay{
    filter: brightness(1.5);
  }
</style>

效果如图:

文内图片

文内图片


目前来说,作者就添加了这么点功能,要是以后探索到更多有趣的功能,会更新这篇文章,所以,愿意订阅我的博客吗?