前言
2024年底,突然对游戏没了兴趣,在家里装了一台Homelab的机器后,开始广泛的对各种技术重新开始产生兴趣。在网上搜资料看到众多大佬自己的网站,找到了很多宝藏,心血来潮的有了自己建一个博客的想法。于是,拿出了被丢掉很多年的建站技能,和废弃很多年的域名(后来申请了一个新的),搭建了一个个人博客站,也就有了经典的博客第一篇文章就是搭建博客,meta-blog!
全文讲分为3个部分进行介绍,分别为:
- 扬帆起航(介绍博客的搭建和自动化部署)
- 船内软装(介绍博客主题的功能拓展与界面美化)
- 驾驶体验(介绍本地文本编辑工具与博客的融合)
也可以前往 【我的个人站点】 进行阅读。
由于搭建过程中参考众多且没有去细究最初来源,如未列出某些原创内容请与我联系添加。
接下来,就是万字解析、堪称保姆级的Hugo博客搭建指南了。
扬帆起航
原文地址: www.zanks.link/2025/01/01/...
PS: 按以下教程无需域名也能搭建
阅读前请审视整体方案,需要会使用Github和命令行
- 建站工具: hugo
- 建站工具皮肤: PaperMod
- 源代码(原文档)托管平台: Github
- 站点部署: Github Pages
- CI/CD(持续集成/持续部署): Github Actions
整体思路为,在Github中创建两个代码仓,一个用于管理源代码(博主的工作台,建议设置为Private),另一个用于部署静态站点(需要特殊命名,利用Github Pages托管),再利用Github Actions监听源代码仓变动后,自动更新Github Pages页面。
Hugo安装及配置
以下命令均由GitBash终端执行,如使用CMD或PowerShell可以自行替换对应命令
Hugo安装
Hugo的安装比较简单,go的编译产物基本都是一个二进制的可执行文件。从Github的Release页面下载对应操作系统的可执行文件即可。
我常用的工作、开发机操作系统均为Windows,则以Windows为例,下载 hugo的windows版本文件,将.exe文件解压到任意位置,如D:\sdk\hugo
。打开环境变量配置,在PATH变量中新增D:\sdk\hugo
目录,使得命令行可以直接使用hugo.exe
而不需要CD到指定目录(不会新增环境变量的,请自行百度)。
下载及配置完之后,打开命令行,输入hugo version
或 hugo.exe version
进行验证,如输出版本号则代表安装成功。例如我的输出为:
bash
$ hugo version
hugo v0.139.3-2f6864387cd31b975914e8373d4bf38bddbd47bc+extended+withdeploy windows/amd64 BuildDate=2024-11-29T15:36:56Z VendorInfo=gohugoio
创建站点
使用hugo命令 hugo new site $YOUR_SITE_NAME
来创建站点。该站点后续会被作为原文档仓库托管到Github进行管理
bash
$ hugo new site zanks-blog
Congratulations! Your new Hugo site was created in D:\03_code\personal\zanks-blog.
Just a few more steps...
1. Change the current directory to D:\03_code\personal\zanks-blog.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Or, install a theme from https://themes.gohugo.io/
3. Edit hugo.toml, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content <SECTIONNAME>\<FILENAME>.<FORMAT>".
5. Start the embedded web server with the command "hugo server --buildDrafts".
See documentation at https://gohugo.io/.
命令输入完成后,会在当前目录下创建zanks-blog文件夹,这个文件夹会作为原文档仓库。使用tree
命令(没有的也可以直接用ll
)看一下hugo生成的目录,内容不多。
bash
$ tree .
.
|-- archetypes
| `-- default.md
|-- assets
|-- content
|-- data
|-- hugo.toml
|-- i18n
|-- layouts
|-- static
`-- themes
8 directories, 2 files
配置皮肤
在正式启动前,可以给站点安装一个自己喜欢的皮肤(如果不安装,直接进行下一步本地调试,会出现Page Not Found的错误,我已经试过了,不必再试)
我使用的皮肤是PaperMode
首先,使用git init将这个站点目录变为Git仓库,然后使用submodule
命令获取皮肤,这边会从Github上下载东西,如果遇到网络问题,需要使用魔法手段解决。
这里稍微解释一下,submodule 是一个不太常见的Git命令,通常被用于管理Git仓库中的子模块(submodule)。子模块是指一个Git仓库作为另一个Git仓库的子目录。使用子模块,可以将一个项目嵌入到另一个项目中,同时保持两者的独立性。(AI告诉我的,其实我也不懂)
bash
$ git init
Initialized empty Git repository in D:/03_code/personal/zanks-blog/.git/
$ git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
Cloning into 'D:/03_code/personal/zanks-blog/themes/PaperMod'...
remote: Enumerating objects: 139, done.
remote: Counting objects: 100% (139/139), done.
remote: Compressing objects: 100% (98/98), done.
remote: Total 139 (delta 36), reused 121 (delta 36), pack-reused 0 (from 0)
Receiving objects: 100% (139/139), 249.18 KiB | 5.08 MiB/s, done.
Resolving deltas: 100% (36/36), done.
warning: in the working copy of '.gitmodules', LF will be replaced by CRLF the next time Git touches it
根据PaperMod的官方建议,使用yaml替换toml作为配置文件。安装主题后,备份hugo.toml文件,新增hugo.yml并添加以下内容,其中title可以随意替换为自己喜欢的。
yml
baseUrl: https://example.org/
languageCode: zh-cn
title: 乱话三千
theme: PaperMod
以上配置只能保证站点可以启动,下文中将给出更多高阶配置,需要理解并筛选和更改为适用于自己站点的配置项(当然你直接照抄也是可以的)
yaml
baseUrl: https://example.org/
languageCode: zh-cn # en-us
title: 乱话三千
theme: PaperMod
enableRobotsTXT: true # 允许爬虫协议
enableEmoji: true # 允许使用Emoji表情
buildDrafts: false
buildFuture: false
buildExpired: false
params:
profileMode:
enabled: true
subtitle: "记载着某个人的胡言乱语"
imageUrl: "/homepage.jpg" # optional
imageWidth: 150 # custom size
imageHeight: 150 # custom size
buttons:
- name: 点击开始
url: "/posts"
menu:
main:
- identifier: post
name: 文章
url: /posts/
weight: 10
- identifier: tags
name: 标签
url: /tags/
weight: 20
- identifier: about
name: 关于
url: /about/
weight: 30
这份配置使用了ProfileMode,在首页配置了一个图标和一个按钮,按钮将链接到 /posts 路径。并且在右上角的菜单中添加了三个按钮,分别连接到对应的路径,其中weight表示排序权重,数字越小按钮越靠近左侧。
创建文章
使用 hugo new $YOUR_ARTICAL
命令来创建文章
bash
$ hugo new posts/test.md
Content "D:\\03_code\\personal\\zanks-blog\\content\\posts\\test.md" created
该命令在content/posts目录下创建了test.md
文件,可以再使用 tree content
命令或者 ll content
查看
bash
$ tree content/
content/
|-- posts
| `-- test.md
`-- posts.md
1 directory, 2 files
然后再使用cat
命令查看自动生成的文件中有什么内容
bash
$ cat content/posts/test.md
+++
date = '2024-12-09T09:25:05+08:00'
draft = true
title = 'Test'
+++
其中date、titile很好理解,分别为日期和文章的标题。draft 这个查阅了一下官网的解释,大致的意思为表示当前的文章为草案,在构建时除非人为添加参数指定,否则不会打包到最后的静态文件中。以下是官网的原文解释:
javascript
draft:
(`bool`) If `true`, the page will not be rendered unless you pass the `--buildDrafts` flag to the `hugo` command. Access this value from a template using the [`Draft`](https://gohugo.io/methods/page/draft/) method on a `Page` object.
然后,给文件中,添加一些内容。这里不需要修改draft,后续在编译中可以通过参数指定查看到草稿内容。但生产环境需要发布时,将draft的值改为false
bash
$ cat content/test.md
+++
date = '2024-12-09T09:25:05+08:00'
draft = true
title = 'Test'
+++
这是一篇测试文章
本地调试
本地调试是hugo提供的非常方便的工具,可以让我们在正式发布前看到站点的样子。只需要执行 hugo server
或 hugo serve
即可在本地启动服务端(在调试时可以添加-D参数看到草稿中的文章)。以下是我进行尝试的输出。
bash
$ hugo serve -D
Watching for changes in D:\03_code\personal\zanks-blog\{archetypes,assets,content,data,i18n,layouts,static,themes}
Watching for config changes in D:\03_code\personal\zanks-blog\hugo.yml
Start building sites ...
hugo v0.139.3-2f6864387cd31b975914e8373d4bf38bddbd47bc+extended+withdeploy windows/amd64 BuildDate=2024-11-29T15:36:56Z VendorInfo=gohugoio
| EN
-------------------+-----
Pages | 11
Paginator pages | 0
Non-page files | 0
Static files | 1
Processed images | 0
Aliases | 0
Cleaned | 0
Built in 64 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
启动后,终端输出了服务器地址,使用浏览器进入就可以预览站点部署后的样子了。不出意外的话,可以看到创建的测文章。
配置文章模板
在archetypes目录下备份并修改default.md文件来修改新增文章的模板,以下是我根据官方模板进行修改的模板
yaml
---
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
date: 2020-09-15T11:30:03+00:00
tags: [""]
author: "Zanks"
draft: true # 默认为草稿模式
weight: #可以用于置顶
showToc: true # 显示目录
TocOpen: false # 打开目录
comments: false # 评论
description: ""
searchHidden: false # 优化SEO
ShowReadingTime: true
ShowWordCount: true
---
添加About页面
前面已经配置了About的链接,但是在访问时会出现404,下文中将会配置这个页面。 依然是使用hugo new
命令来创建md文件
bash
$ hugo new about.md
Content "D:\\03_code\\personal\\zanks-blog\\content\\about.md" created
在其中添加一些介绍自己的内容,然后进入页面,点击关于就可以看到新增加到内容了(在发布时,记得将draft更改为false)
我尝试直接在content目录下创建 about.md 文件,在调试环境中可以看到这个页面,但是实际发布后却没有在public目录找到对应文件,暂时没有找到解决方案。删除并配置了文章模板后,通过命令创建about.md后可以正常工作。
源代码仓管理
本地仓初始化
- 执行
git init
命令来初始化目录为git仓
bash
$ git init
Initialized empty Git repository in D:/03_code/personal/zanks-blog/.git/
- 添加一个.gitignore文件,将public文件夹取消跟踪,该仓将作为源代码仓进行管理,所以构建产物不需要加入跟踪。并将该文件commit(建议通过VSCode等带有图形化管理的操作)
bash
$ cat .gitignore
/public
- 为每个空的目录添加.gitkeep文件,来保证源代码仓的完整目录结构(asset、data、i18n、layouts),提交所有改动文件。
完成以上动作之后,本地仓构建完成。后面将在Github上创建代码仓,与该本地仓进行关联。
Github代码仓创建
在Github上创建一个空代码仓,使用命令进行关联。在Github页面上会有提示如何关联已有仓库,根据给出的命令直接复制到终端执行即可。
bash
$ git remote add origin git@github.com:ZaNksC/zanks-blog.git
git branch -M main
git push -u origin main
Enumerating objects: 165, done.
Counting objects: 100% (165/165), done.
Delta compression using up to 12 threads
Compressing objects: 100% (148/148), done.
Writing objects: 100% (165/165), 274.80 KiB | 1.01 MiB/s, done.
Total 165 (delta 36), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (36/36), done.
To github.com:ZaNksC/zanks-blog.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'
登录Github就可以看到该代码仓中已经有内容了。
完成这一步后,就可以在任何装有Git和Hugo的机器上对博客进行开发了。
自动化部署
Github Pages托管
使用Github Pages进行静态站点托管,需要在Github中创建一个名为 username.github.io
的特殊项目。 创建之后,就可以用 https://username.github.io
作为域名直接访问了,当然现在这里是一个空项目,访问之后会出现404。
Github Action自动化部署
Github的Action是一套标准的CI/CD系统,通过内置在源代码仓中.github/workflow
目录下的yaml文件来定义流水线的动作。以下是我从Github Workflow的hugo流水线模板修改来的CICD一体化流水线。
bash
$ cat .github/workflows/hugo.yml
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hugo site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.139.3
steps:
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Install Dart Sass
run: sudo snap install dart-sass
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build with Hugo
env:
HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
HUGO_ENVIRONMENT: production
run:
hugo --baseURL=https://zanksc.github.io
- name: Deploy Pages
uses: peaceiris/actions-gh-pages@v3
with:
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
EXTERNAL_REPOSITORY: ZaNksC/ZaNksC.github.io
PUBLISH_BRANCH: main
PUBLISH_DIR: ./public
commit_message: ${{ github.event.head_commit.message }}
需要自行修改 Build With Hugo 步骤中的构建命令,baseURL替换为自己的博客地址,Deploy Pages步骤中的EXTERNAL_REPOSITORY变量,需要替换为自己的GitPage地址。
需要注意的是,在最后一个步骤Deploy Pages中,有${{ secrets.PERSONAL_TOKEN }}
变量,这个变量位于源代码仓-settings-Secrete And variables-Action中。需要在这个目录中创建一个名为PERSONAL_TOKEN的变量。 变量的值则需要在Github(点击头像) - settings-Deploy Settings-Personal access tokens中创建,创建一个经典(classic)的Token,赋予 repo 的所有权限保证可以读写仓库。复制该Token的值(只会出现一次 ),填入上文提到的 PERSONAL_TOKEN的变量中。
在全部配置完成之后,可以通过 hugo new posts/test.md
输入一些内容并将draft值改为true,输入 git add . && git commit -m "add test.md"
进行提交,然后等待一段时间后,访问 https://name.github.io
查看博客中是否出现该文章来进行全流程验证。
船内软装
搭建完博客之后,总是会觉得这不满意,然后在搭建过程中,用相同的建站工具和主题搜索,会发现网上有很多大佬已经搭建好的站点,就会这个功能也想要,那个功能也想要。在这里,我记录了一些我搭建过程中进行基础功能拓展和美化的过程。希望可以帮助到有缘人~
以下教程为进阶版的简略教程,需要你有比较扎实的编程基础(或者不爱思考的脑袋),很多教程来自网上其他大佬,由于看了太多的教程,链接引用不全请见谅,如果有大佬看到了可以联系我加上。
以下修改html和css的代码都不建议在themes文件夹直接修改,html可以在layout目录下新建同名文件(复制过来改),css可以在assets目录下新建,最终构建都会打包到public中并使用自定义的覆盖主题文件夹中的内容。
好了,接下来就是博客裁缝的展示环节了。
拓展功能
评论
Gisucs
选用Gisucs作为评论服务,最开始我的选择是Gisucs,但是后来我放弃了。Github作为存储虽然是全免费,非常优雅,但是要求评论者拥有一个Github账号对于用户体验实在是不太友好。
Github配置
参考链接: giscus.app/zh-CN#repos...
- 在某个Github公开仓库开启discussion功能(settings进入)
- 在Github安装Gisucs APP
- 记录生成的配置和代码,需要配置到自己的项目中
Hugo配置
参考链接: www.tofuwine.cn/posts/610b7...
- 添加文件
layouts/partials/comments.html
,输入以下内容
html
<div class="comments-title" id="tw-comment-title">
<p class="x-comments-title">{{- .Param "giscus.discussionTitle" }}</p>
<p style="font-size: 1rem">{{- .Param "giscus.discussionSubtitle" }} </p>
</div>
<div id="tw-comment"></div>
<script>
const getStoredTheme = () => localStorage.getItem("pref-theme") === "dark" ? "{{ .Site.Params.giscus.darkTheme }}" : "{{ .Site.Params.giscus.lightTheme }}";
const setGiscusTheme = () => {
const sendMessage = (message) => {
const iframe = document.querySelector('iframe.giscus-frame');
if (iframe) {
iframe.contentWindow.postMessage({giscus: message}, 'https://giscus.app');
}
}
sendMessage({setConfig: {theme: getStoredTheme()}})
}
document.addEventListener("DOMContentLoaded", () => {
const giscusAttributes = {
"src": "https://giscus.app/client.js",
"data-repo": "{{ .Site.Params.giscus.repo }}",
"data-repo-id": "{{ .Site.Params.giscus.repoId }}",
"data-category": "{{ .Site.Params.giscus.category }}",
"data-category-id": "{{ .Site.Params.giscus.categoryId }}",
"data-mapping": "{{ .Site.Params.giscus.mapping }}",
"data-strict": "{{ .Site.Params.giscus.strict }}",
"data-reactions-enabled": "{{ .Site.Params.giscus.reactionsEnabled}}",
"data-emit-metadata": "{{ .Site.Params.giscus.emitMetadata }}",
"data-input-position": "{{ .Site.Params.giscus.inputPosition }}",
"data-theme": getStoredTheme(),
"data-lang": "{{ .Site.Params.giscus.lang }}",
"data-loading": "lazy",
"crossorigin": "anonymous",
"async": "",
};
// 动态创建 giscus script
const giscusScript = document.createElement("script");
Object.entries(giscusAttributes).forEach(
([key, value]) => giscusScript.setAttribute(key, value));
document.querySelector("#tw-comment").appendChild(giscusScript);
// 页面主题变更后,变更 giscus 主题
const themeSwitcher = document.querySelector("#theme-toggle");
if (themeSwitcher) {
themeSwitcher.addEventListener("click", setGiscusTheme);
}
const themeFloatSwitcher = document.querySelector("#theme-toggle-float");
if (themeFloatSwitcher) {
themeFloatSwitcher.addEventListener("click", setGiscusTheme);
}
});
</script>
- 添加文件
assets/css/extended/comments.css
,输入以下内容
css
/* giscus 评论组件 */
.comments-title {
margin-top: 2rem;
margin-bottom: 2rem;
display: block;
text-align: center;
}
.x-comments-title {
display: block;
font-size: 1.25em;
font-weight: 700;
padding: 1.5rem 0 .5rem;
}
- 在
hugo.yaml
中添加内容,其中{{}}
包裹的需要替换成自己的内容,内容在Gisucs会生成
yaml
giscus:
repo: "{{ REPO }}"
repoId: "{ REPO_ID }"
category: "Announcements"
categoryId: "{{ CATEGORYID }}"
mapping: "pathname"
strict: "0"
reactionsEnabled: "1"
emitMetadata: "0"
inputPosition: "bottom"
lightTheme: "light"
darkTheme: "dark"
lang: "zh-CN"
discussionTitle: 欢迎来到评论区
discussionSubtitle: 感谢您的耐心阅读!如需交流,请留个评论吧!
- 在
archetypes/default.md
中添加配置comments: true
来开启评论区,已有的博客也需要修改这个来开启
Twikoo
因为Gisucs要求评论者具有Github账号有点使用者不太友好,所以后面更换了一个Twikoo
Twikoo的整体可以看作是一个CSS的架构,C-Hugo站点的静态JS代码,S-云函数,S-MonogoDB数据库。Twikoo的官方是懂我们这些穷鬼的,提供了很多免费部署的教程。我这边就按照官方推荐度最高的方法进行部署。MonogoDB选择MongoDB Atlas,有500MB的免费额度。Netlify部署云函数,每月 125,000 请求次数和 100 小时函数计算时长,看上去是完全够了。
MongoDB Atlas 申请资源
按照Twikoo官网指导,申请一个账号和500MB空间的MonogoDB数据库。
- 注册MongoDB Atlas账号
- 无脑下一步,创建一个MonogoDB实例,这里创建数据库用户的时候密码记住,后面要用
- 修改Network Access为 0.0.0.0 允许所有IP访问
- 在connect中查看代码示例,选Node.js可以看到连接串,把数据库用户密码替换进去,把整个URL复制下来,等部署云函数的时候有用
Netlify 部署云函数
- 注册Netlify账号,注册的时候需要提供身份验证(有点恶心人了),密码需要设置得复杂一点,不然直接不给登录也不提示,我通过忘记密码改了一个很复杂的。(后来证明我第二次登录就忘记了)
- Fork官方的仓到自己的Github,从仓库部署站点,环境变量中填入
MONGODB_URI
值为MonoDB Atlas中复制出来的URL - 点xxx.netlify.app进入页面,看到云函数正常运行就是部署成功了
前端部署
需要把前端JS嵌入到Hugo的Html页面中,在 layout/comments.html
文件中添加代码 (如果有其他评论组件请自行删除)
html
<div id="tcomment"></div>
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.6.40/dist/twikoo.all.min.js"></script>
<script>
twikoo.init({
envId: 'https://xxx.netlify.app/.netlify/functions/twikoo',
el: '#tcomment', // 容器元素
})
</script>
使用配置
- 进入管理员界面,首次进入配置密码
- 配置邮箱通知,Hotmail配置了无法测试通过,暂时没找到原因,后来配置了一个163的邮箱(需要填授权码,听说qq更方便一点,gmail没有尝试)
- 反垃圾配置,使用默认的反垃圾服务 akismet,支付一个0元账单就可以获得一个APPKey,填入配置里面就好
- 头像服务,默认的 weavatar,注册一个账号,上传一个自己喜欢的头像。但是这边遇到了一个比较奇怪的问题,同时输入昵称和邮箱,头像就会读取失败(但是后来发现生产上好像可以),所以我在通用设置里面把必填字段改成了只填邮箱,因为昵称填QQ号会自动生成qq邮箱。
侧面显示目录
参考资料: www.zhouxin.space/logs/introd...
- 创建文件
layouts/partials/toc.html
输入以下代码(代码全是抄的,别问我为什么这么写)
html
{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<aside id="toc-container" class="toc-container wide">
<div class="toc">
<details {{if (.Param "TocOpen") }} open{{ end }}>
<summary accesskey="c" title="(Alt + C)">
<span class="details">{{- i18n "toc" | default "Table of Contents" }}</span>
</summary>
<div class="inner">
{{- $largest := 6 -}}
{{- range $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}
{{- if lt $headerLevel $largest -}}
{{- $largest = $headerLevel -}}
{{- end -}}
{{- end -}}
{{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}
{{- $.Scratch.Set "bareul" slice -}}
<ul>
{{- range seq (sub $firstHeaderLevel $largest) -}}
<ul>
{{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
{{- end -}}
{{- range $i, $header := $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}
{{/* get id="xyz" */}}
{{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}
{{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
{{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
{{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}
{{- if ne $i 0 -}}
{{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
{{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
{{- if gt $headerLevel $prevHeaderLevel -}}
{{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
<ul>
{{/* the first should not be recorded */}}
{{- if ne $prevHeaderLevel . -}}
{{- $.Scratch.Add "bareul" . -}}
{{- end -}}
{{- end -}}
{{- else -}}
</li>
{{- if lt $headerLevel $prevHeaderLevel -}}
{{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
{{- if in ($.Scratch.Get "bareul") . -}}
</ul>
{{/* manually do pop item */}}
{{- $tmp := $.Scratch.Get "bareul" -}}
{{- $.Scratch.Delete "bareul" -}}
{{- $.Scratch.Set "bareul" slice}}
{{- range seq (sub (len $tmp) 1) -}}
{{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
{{- end -}}
{{- else -}}
</ul>
</li>
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
{{- else }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
{{- end -}}
{{- end -}}
<!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
{{- $firstHeaderLevel := $largest }}
{{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
</li>
{{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
{{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
</ul>
{{- else }}
</ul>
</li>
{{- end -}}
{{- end }}
</ul>
</div>
</details>
</div>
</aside>
<script>
let activeElement;
let elements;
document.addEventListener('DOMContentLoaded', function (event) {
checkTocPosition();
elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
if (elements.length > 0) {
// Make the first header active
activeElement = elements[0];
const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
}
// Add event listener for the "back to top" link
const topLink = document.getElementById('top-link');
if (topLink) {
topLink.addEventListener('click', (event) => {
// Prevent the default action
event.preventDefault();
// Smooth scroll to the top
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
}, false);
window.addEventListener('resize', function(event) {
checkTocPosition();
}, false);
window.addEventListener('scroll', () => {
// Get the current scroll position
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
// Check if the scroll position is at the top of the page
if (scrollPosition === 0) {
return;
}
// Ensure elements is a valid NodeList
if (elements && elements.length > 0) {
// Check if there is an object in the top half of the screen or keep the last item active
activeElement = Array.from(elements).find((element) => {
if ((getOffsetTop(element) - scrollPosition) > 0 &&
(getOffsetTop(element) - scrollPosition) < window.innerHeight / 2) {
return element;
}
}) || activeElement;
elements.forEach(element => {
const id = encodeURI(element.getAttribute('id')).toLowerCase();
const tocLink = document.querySelector(`.inner ul li a[href="#${id}"]`);
if (element === activeElement){
tocLink.classList.add('active');
// Ensure the active element is in view within the .inner container
const tocContainer = document.querySelector('.toc .inner');
const linkOffsetTop = tocLink.offsetTop;
const containerHeight = tocContainer.clientHeight;
const linkHeight = tocLink.clientHeight;
// Calculate the scroll position to center the active link
const scrollPosition = linkOffsetTop - (containerHeight / 2) + (linkHeight / 2);
tocContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' });
} else {
tocLink.classList.remove('active');
}
});
}
}, false);
const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);
function checkTocPosition() {
const width = document.body.scrollWidth;
if (width - main - (toc * 2) - (gap * 4) > 0) {
document.getElementById("toc-container").classList.add("wide");
} else {
document.getElementById("toc-container").classList.remove("wide");
}
}
function getOffsetTop(element) {
if (!element.getClientRects().length) {
return 0;
}
let rect = element.getBoundingClientRect();
let win = element.ownerDocument.defaultView;
return rect.top + win.pageYOffset;
}
</script>
{{- end }}
- 创建
assets/css/extended/toc.css
文件,并输入以下内容(也是抄的,感谢大佬,赞美大佬)
css
:root {
--nav-width: 1380px;
--article-width: 650px;
--toc-width: 300px;
}
.toc {
margin: 0 2px 40px 2px;
border: 1px solid var(--border);
background: var(--entry);
border-radius: var(--radius);
padding: 0.4em;
}
.toc-container.wide {
position: absolute;
height: 100%;
border-right: 1px solid var(--border);
left: calc((var(--toc-width) + var(--gap)) * -1);
top: calc(var(--gap) * 2);
width: var(--toc-width);
}
.wide .toc {
position: sticky;
top: var(--gap);
border: unset;
background: unset;
border-radius: unset;
width: 100%;
margin: 0 2px 40px 2px;
}
.toc details summary {
cursor: zoom-in;
margin-inline-start: 20px;
padding: 12px 0;
}
.toc details[open] summary {
font-weight: 500;
}
.toc-container.wide .toc .inner {
margin: 0;
}
.active {
font-size: 110%;
font-weight: 600;
}
.toc ul {
list-style-type: circle;
}
.toc .inner {
margin: 0 0 0 20px;
padding: 0px 15px 15px 20px;
font-size: 16px;
/*目录显示高度*/
max-height: 83vh;
overflow-y: auto;
}
.toc .inner::-webkit-scrollbar-thumb { /*滚动条*/
background: var(--border);
border: 7px solid var(--theme);
border-radius: var(--radius);
}
.toc li ul {
margin-inline-start: calc(var(--gap) * 0.5);
list-style-type: none;
}
.toc li {
list-style: none;
font-size: 0.95rem;
padding-bottom: 5px;
}
.toc li a:hover {
color: var(--secondary);
}
- 同时在
archetypes\default.yml
中添加参数
yaml
showToc: true # 显示目录
TocOpen: true # 打开目录
PV/UV
不蒜子Busuanzi
使用不蒜子Busuanzi进行站点访问统计。 参考链接: blog.kanikig.xyz/hugo-busuan...
- 添加文件
layouts\parials\head.html
从主题文件夹themes\layouts\parials\head.html
中拷贝内容并修改,在Styles上方添加一个if块
html
...
{{- if site.Params.analytics.naver.SiteVerificationTag }}
<meta name="naver-site-verification" content="{{ site.Params.analytics.naver.SiteVerificationTag }}">
{{- end }}
<!-- 以下If块为新增内容 -->
{{- if .Site.Params.busuanzi.enable -}}
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<meta name="referrer" content="no-referrer-when-downgrade">
{{- end -}}
{{- /* Styles */}}
...
- 添加文件
layouts\parials\footer.html
从主题文件夹themes\layouts\parials\footer.html
中拷贝内容并修改,在footer标签内添加一个if块
html
...
{{- if not (.Param "hideFooter") }}
<footer class="footer">
{{- if not site.Params.footer.hideCopyright }}
{{- if site.Copyright }}
<span>{{ site.Copyright | markdownify }}</span>
{{- else }}
<span>© {{ now.Year }} <a href="{{ "" | absLangURL }}">{{ site.Title }}</a></span>
{{- end }}
{{- print " · "}}
{{- end }}
{{- with site.Params.footer.text }}
{{ . | markdownify }}
{{- print " · "}}
{{- end }}
<!-- 以下IF块为新增内容 -->
{{ if .Site.Params.busuanzi.enable -}}
<div class="busuanzi-footer">
<span id="busuanzi_container_site_pv">
本站总访问量<span id="busuanzi_value_site_pv"></span>次
</span>
<span id="busuanzi_container_site_uv">
本站访客数<span id="busuanzi_value_site_uv"></span>人次
</span>
</div>
{{- end -}}
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
</span>
</footer>
...
{{- end }}
- 添加文件
layouts\_defaults\single.html
从主题文件夹themes\layouts\_defaults\single.html
中拷贝内容并修改,在post-meta内添加一个if块
html
...
<div class="post-meta">
{{- partial "post_meta.html" . -}}
{{- partial "translation_list.html" . -}}
{{- partial "edit_post.html" . -}}
{{- partial "post_canonical.html" . -}}
<!-- 以下If块为新增内容 -->
{{ if .Site.Params.busuanzi.enable -}}
<div class="meta-item"> · 
<span id="busuanzi_container_page_pv">本文阅读量<span id="busuanzi_value_page_pv"></span>次</span>
</div>
{{- end }}
</div>
...
- 在
hugo.html
文件中的param节点中添加开关
yaml
params:
busuanzi:
enable: true
Google Analyze
只需要在 hugo.yaml
中添加以下代码即可,id更换为自己在Google Analytic中申请的跟踪ID
yaml
services:
googleAnalytics:
id: "G-XXXXXXXXXX"
时间线
添加一个根据时间线排列的归档页面,写的多写的久才有质感。多写写吧!
- 添加
content\archive.md
文件,输入以下内容
markdown
---
title: ""
layout: "archives"
url: "/archives/"
summary: archives
---
- 在
hugo.html
中添加按钮,在menu.main
节点下新增以下内容
yaml
- identifier: archive
name: 时间轴
url: /archives/
weight: 11
留言板
利用评论系统,新增一个Layout来构建一个留言板页面。
- 创建
layouts\message.html
文件输入一下代码(代码从single.html
中删除了很多东西,简化得到,顺便做了一下标题居中,这回是自己写的了):
html
{{- define "main" }}
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
<div style="margin-left: auto;margin-right: auto;">
{{ .Title }}
</div>
</h1>
</header>
{{- if .Content }}
<div class="post-content">
{{ .Content }}
</div>
{{- end }}
<footer class="post-footer">
<div id="tcomment"></div>
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.6.40/dist/twikoo.all.min.js"></script>
<script>
twikoo.init({
envId: 'xxx',
el: '#tcomment',
})
</script>
</footer>
</article>
{{- end }}
- 在命令行中创建一个新的md文件(不要直接新建文件,我试了,编译的时候不会生成文件,会导致404),输入
hugo new message.md
,并在其中输入以下代码
markdown
---
title: "💬 留言板"
date: '2024-12-13T21:14:22+08:00'
draft: false # 默认为草稿模式
layout: "message"
comments: true # 评论
---
- 在
hugo.yaml
中新增首页目录,添加以下代码
yaml
- identifier: message
name: 💬 留言板
url: /message/
weight: 21
添加最近修改时间
复制主题中的 post_meta.html
到 layout/partials/post_meta.html
新增最近修改时间的内容,并修改发布时间的内容和国际化,
- 在
layout/partials/post_meta.html
输入以下代码
html
{{- $scratch := newScratch }}
{{- if not .Date.IsZero -}}
{{- $dateStr := .Date.Format (default "January 2, 2006" site.Params.DateFormat) -}}
{{- $translatedPublished := i18n "published" -}}
{{- $dateHTML := printf "<span title='%s'>%s</span>" (.Date) $dateStr -}}
{{- $scratch.Add "meta" (slice (printf "%s %s" $translatedPublished $dateHTML)) }}
{{- end }}
{{- if not .Lastmod.IsZero -}}
{{- $lastmodStr := .Lastmod.Format (default "January 2, 2006" site.Params.DateFormat) -}}
{{- $translatedLastModified := i18n "last_modified" -}}
{{- $lastmodHTML := printf "<span title='%s'>%s</span>" (.Lastmod) $lastmodStr -}}
{{- $scratch.Add "meta" (slice (printf "%s %s" $translatedLastModified $lastmodHTML)) }}
{{- end }}
{{- if (.Param "ShowReadingTime") -}}
{{- $scratch.Add "meta" (slice (i18n "read_time" .ReadingTime | default (printf "%d min" .ReadingTime))) }}
{{- end }}
{{- if (.Param "ShowWordCount") -}}
{{- $scratch.Add "meta" (slice (i18n "words" .WordCount | default (printf "%d words" .WordCount))) }}
{{- end }}
{{- if not (.Param "hideAuthor") -}}
{{- with (partial "author.html" .) }}
{{- $scratch.Add "meta" (slice .) }}
{{- end }}
{{- end }}
{{- with ($scratch.Get "meta") }}
{{- delimit . " · " | safeHTML -}}
{{- end -}}
- 在i18n文件夹的对应目录添加翻译
- 在
hugo.yaml
中新增配置frontmatte.lastmod: ['lastmod', ':git', 'date', 'publishDate']
用于指定lastmod变量的赋值顺序
自定义域名
首先需要有一个域名,在对应的域名服务商添加一条解析记录,解析类型:CNAME,解析值为 xxx.github.io (xxx替换为自己的Github用户名)
再添加文件 static\CNAME
,文件的内容为自定义的域名。没有这个文件,访问域名会出现404。
友链
参考: aimerneige.com/zh/post/web...
这个是没有脑子直接抄的
- 创建一个文件
layouts/partials/friends.html
输入以下代码
html
<style type="text/css">
.friends {
--link-count-per-row: 1;
}
@media screen and (min-width: 576px) {
.friends {
--link-count-per-row: 2;
}
}
@media screen and (min-width: 768px) {
.friends {
--link-count-per-row: 3;
}
}
.friends {
display: grid;
grid-template-columns: repeat(var(--link-count-per-row), 1fr);
grid-gap: 16px;
}
/* 空间占位 */
.friend-skeleton {
height: 280px;
display: inline-block;
position: relative;
}
.friend {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
transition: 0.67s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: var(--radius);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12) !important;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.friend:hover {
transform: translateY(-8px);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px 0 rgba(0, 0, 0, 0.14), 0 1px 14px 0 rgba(0, 0, 0, 0.12) !important;
}
.friend-avatar {
object-fit: cover;
width: 100%;
height: 180px;
margin: 0 !important;
border-radius: 0 !important;
}
.friend-content {
text-align: center;
flex: 1;
width: 100%;
padding: 16px;
background: var(--entry);
transform: translate3d(0, 0, 0);
}
.friend-name {
font-size: 1.2rem;
font-weight: bold;
transform: inherit;
}
.friend-description {
font-size: 0.8rem;
color: var(--secondary);
transform: translate3d(0, 0, 0);
}
</style>
<div class="friends">
{{ range .Site.Data.friends }}
<div class="friend-skeleton">
<a href="{{ .link }}" target="_blank">
<div class="friend">
<img class="friend-avatar" src="{{ .image }}" />
<div class="friend-content">
<div class="friend-name">{{ .title }}</div>
<div class="friend-description">{{ .intro }}</div>
</div>
</div>
</a>
</div>
{{ end }}
</div>
<!-- style code by https://github.com/fissssssh -->
<!-- view https://github.com/fissssssh/fissssssh.github.io for more detail -->
- 创建
layouts/_default/friends.html
文件,输入一下代码,用于创建新的界面
html
{{- define "main" }}
<article class="post-single">
<header class="post-header">
{{ partial "breadcrumbs.html" . }}
<h1 class="post-title entry-hint-parent">
{{ .Title }}
</h1>
</header>
{{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
{{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}
{{- if (.Param "ShowToc") }}
{{- partial "toc.html" . }}
{{- end }}
<div style="height: 40px;"></div>
{{ .Content }}
<div style="height: 40px;"></div>
{{ partial "friends.html" . }}
<div style="height: 40px;"></div>
{{- if (.Param "comments") }}
{{- partial "comments.html" . }}
{{- end }}
</article>
{{- end }}{{/* end main */}}
- 创建
content/posts/friends.md
文件,输入以下内容
yaml
---
title: "🧑🤝🧑 Zanks的朋友们 🧑🤝🧑"
date: '2024-01-01T00:00:00+08:00'
author: "Zanks"
layout: "friends"
comments: true # 评论
---
排名不分先后,按时间顺序添加。如需新增请留言,或通过其他方式联系我。
- 创建
data/friends.yml
文件,用于存储友链的数据,以下仅为示范
yaml
- title: "伞"
intro: "一只咸鱼的学习记录"
link: "https://umb.ink/"
image: "https://avatars.githubusercontent.com/u/53655863?v=4"
- title: "HelloWorld的小博客"
intro: "这里是一个小白的博客"
link: "https://mzdluo123.github.io/"
image: "https://avatars.githubusercontent.com/u/23146087?v=4"
全站文章和字数统计
参考:huuuuuuo-github-io.vercel.app/post/hugo%E...
可以将以下代码加入到任何喜欢的页面,我放在了About页面
html
{{$scratch := newScratch}}
{{ range (where .Site.RegularPages "Section" "posts" )}}
{{$scratch.Add "total" .WordCount}}
{{ end }}
<div>
<div>
📚 文章数:
</div>
<div>
{{ len (where .Site.RegularPages "Section" "posts") }}
</div>
</div>
<div>
<div>
✍️ 总字数:
</div>
<div>
{{ div ($scratch.Get "total") 1000.0 | lang.FormatNumber 2 }}K
</div>
</div>
右侧边栏
底部悬浮按钮
参考了一个个性化的PaperMod主题 github.com/tofuwine/Pa... 实现的效果是在页面的右侧添加了一个前往评论区的按钮,重写了官方回到顶部的按钮(添加了阅读进度)
- 在
static\js
目录下引入两个文件分别用于实现按钮的动态样式,实现逻辑来自PaperMod-PE源码。
js
// go-comment.js
document.addEventListener('scroll', function () {
const comment = document.getElementById("comments-title");
const bottomToComment = document.getElementById("comments-link")
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = comment.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
if (top <= viewPortHeight + 100) {
bottomToComment.style.visibility = "hidden";
bottomToComment.style.opacity = "0";
} else {
bottomToComment.style.visibility = "visible";
bottomToComment.style.opacity = "1";
}
})
js
// go-top.js
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = Number(localStorage.getItem("menu-scroll-position"));
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft.toString());
}
}
window.onscroll = function () {
const goTopButton = document.getElementById("top-link");
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
goTopButton.style.visibility = "visible";
goTopButton.style.opacity = "1";
} else {
goTopButton.style.visibility = "hidden";
goTopButton.style.opacity = "0";
}
};
document.addEventListener('scroll', function (e) {
const readProgress = document.getElementById("pe-read-progress");
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
readProgress.innerText = ((scrollTop / (scrollHeight - clientHeight)).toFixed(2) * 100).toFixed(0);
})
- 删除原有回到顶部的按钮,或通过配置disableScrollToTop关闭(代码位于
footer.html
) - 添加
right_aside.html
文件添加右侧边栏的代码
html
<aside>
<!--右侧按钮-->
<div class="right-side-btns">
{{- /* Scroll To Comment */ -}}
<script src="/js/go-comment.js"></script>
<a href="#comments-title" class="right-side-btn" id="comments-link"
style="visibility: visible; opacity: 1;padding: 0.6rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29.338 29.338" fill="currentColor">
<path d="M27.184,1.605H2.156C0.967,1.605,0,2.572,0,3.76v17.572c0,1.188,0.967,2.155,2.156,2.155h13.543
l5.057,3.777c0.414,0.31,0.842,0.468,1.268,0.468c0.789,0,1.639-0.602,1.637-1.923v-2.322h3.523c1.188,0,2.154-0.967,2.154-2.155
V3.76C29.338,2.572,28.371,1.605,27.184,1.605z M27.34,21.332c0,0.085-0.068,0.155-0.154,0.155h-5.523v3.955l-5.297-3.956H2.156
c-0.086,0-0.154-0.07-0.154-0.155V3.759c0-0.085,0.068-0.155,0.154-0.155v0.001h25.029c0.086,0,0.154,0.07,0.154,0.155
L27.34,21.332L27.34,21.332z M5.505,10.792h4.334v4.333H5.505C5.505,15.125,5.505,10.792,5.505,10.792z M12.505,10.792h4.334v4.333
h-4.334V10.792z M19.505,10.792h4.334v4.333h-4.334V10.792z" />
</svg>
</a>
<script src="/js/go-top.js"></script>
<a href="#top" class="right-side-btn" id="top-link" style="text-align: center;">
<span id="pe-read-progress"></span>
</a>
</div>
</aside>
- 添加CSS
asset\css\extended\right_side.css
实现样式
css
:root {
--text-color: #bdbdbd;
--text-hover-color: #373737ac;
--right-side-width: 300px;
}
.dark {
--text-color: #bdbdbd;
--text-hover-color: #373737ac;
}
.right-side-btns,
.right-side-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.right-side-btns {
position: fixed;
right: 1rem;
z-index: 999;
bottom: 2rem;
gap: 1rem;
}
.right-side-btn {
width: 2.4rem;
height: 2.4rem;
padding: 0.3rem;
border-radius: 50%;
background: var(--tertiary);
color: var(--secondary);
transition: all 0.3s ease;
font-size: 0.9rem;
}
.right-side-btn:hover {
background-color: var(--text-color);
color: var(--text-hover-color);
outline: 0;
}
作者信息栏
此部分代码在网上没找到样例可以抄,大部分都是靠着本人一边GPT一边理解自己实现的。 实现了一个可以悬浮在右侧边栏的框,展示头像、社交信息和最近更新的文章。
- 添加一个在
right-side.html
中的<aside>
节点下添加以下代码
html
<div class="author-info">
<!-- 头像 -->
<img src="/homepage.jpg" class="author-avatar" />
<!-- 社交信息 -->
<div>
<div class="social-icons">
{{- range site.Params.socialIcons }}
<a href="{{ trim .url " " | safeURL }}" target="_blank" rel="noopener noreferrer me"
title="{{ (.title | default .name) | title }}">
{{ partial "svg.html" . }}
</a>
{{- end }}
</div>
</div>
<!-- 最近更新的文章 -->
<div class="last-mod">
<div>
<div class="custom-divider"></div>
<div class="last-mod-title">最近更新</div>
<div class="custom-divider"></div>
</div>
<div class="last-mod-table">
{{ range first 3 (where .Site.RegularPages "Section" "posts").ByDate.Reverse }}
<a class="last-mod-item" href="{{ .RelPermalink }}">
<div class="last-mod-item-tile">{{ .Title }} </div>
<div class="last-mod-item-time"> ----- 于 {{.Lastmod.Format (default "January 2, 2006"
site.Params.DateFormat)}} 由 {{ .Params.author }} 更新</div>
</a>
{{ end }}
</div>
</div>
</div>
- 在
asset\css\extended\right_side.css
添加样式
css
.right-side {
position: absolute;
height: 100%;
width: 25%;
border-left: 1px solid var(--border);
right: calc((var(--right-side-width) + var(--gap)) * -1);
top: calc(var(--gap) * 2);
width: var(--right-side-width);
}
.author-info {
display: flex;
flex-direction: column; /* 设置主轴方向为纵向 */
justify-content: center; /* 在主轴上居中对齐 */
align-items: center; /* 在交叉轴上居中对齐 */
margin-top: 0px;
margin-left: 25px;
margin-right: 10px;
position: sticky;
top: var(--gap);
border: 2px solid var(--text-hover-color); /* 边框宽度为2px,样式为实线,颜色为黑色 */
box-shadow: 0 2px 4px var(--text-hover-color); /* 水平偏移0px,垂直偏移4px,模糊半径8px,颜色为黑色且透明度为0.1 */
}
.author-avatar {
height: 128px;
width: 128px;
margin-top: 20px;
border-radius: 50%;
}
.last-mod {
width: 90%;
margin-top: 10px;
}
.last-mod-table {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 5px;
}
.last-mod-title {
text-align: center;
font-weight: bold;
}
.last-mod-item {
width: 100%;
display: 1;
font-size: 0.9rem;
border: 2px solid var(--text-hover-color);
border-radius: 10px;
padding: 4px;
padding-left: 7px;
padding-right: 7px;
margin-bottom: 10px ;
transition: transform 0.5s ease;
}
.last-mod-item:hover {
border: 2px solid var(--text-color);
}
.last-mod-item-time {
font-size: 0.7rem;
text-align: right;
}
.custom-divider {
width: 100%;
height: 1px;
background: repeating-linear-gradient(
to right,
#e0e0e0,
#e0e0e0 33%,
transparent 0,
transparent 67%,
#e0e0e0 0,
#e0e0e0 100%
);
}
代码显示优化
代码来自 tofuwin的个性化主题,仅css略作修改:
html
{{ $defaultFoldMod := .Page.Param "codeBlockFoldMode" | default false }}
{{- $lang := .Type | default "text" }}
{{- $title := .Attributes.title | default "" }}
{{- $u_lang := .Attributes.lang | default "" }}
{{- $hideLang := .Attributes.hide | default false }}
{{ $foldMode := .Attributes.fold | default $defaultFoldMod }}
{{- if and (.Attributes.force) (ne $u_lang "") }}
{{- $lang = $u_lang }}
{{- end }}
<div class="pe-code-block-wrap {{ if ne $foldMode "disable" }} pe-code-details {{ end }} {{ if not $foldMode }} open {{ end }} scrollable">
<div class="pe-code-block-header pe-code-details-summary">
<div class="pe-code-block-header-left">
{{ if ne $foldMode "disable" }}
<i class="arrow fas fa-chevron-right fa-fw pe-code-details-icon" aria-hidden="true"></i>
{{ end }}
{{ if not $hideLang }}
<span>
{{- if ne $u_lang "" }}
{{- $u_lang -}}
{{- else }}{{- $lang -}}{{- end }}
</span>
{{ end }}
</div>
<div class="pe-code-block-header-center">
<span>
{{- if ne $title "" }}
{{- $title }}
{{ end }}
</span>
</div>
<div class="pe-code-block-header-right">
{{ if ne $foldMode "disable" }}
<i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i>
{{ end }}
<button class="pe-code-copy-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="pe-icon"><path fill="currentColor" fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z" clip-rule="evenodd"></path></svg>
</button>
</div>
</div>
<div class="pe-code-details-content scrollable">
{{ highlight .Inner $lang .Options }}
</div>
</div>
html
<pre class="mermaid">
{{- .Inner | safeHTML }}
</pre>
{{ .Page.Store.Set "hasMermaid" true }}
css
:root {
--pe-primary-hover-color: #777777;
/* 代码块标题色 */
--pe-code-block-header-color: var(--primary);
/* 代码块标题背景色 */
--pe-code-block-header-bg-color: #ededed;
/* 代码块文本颜色 */
--pe-code-block-color: #979797;
/* 代码块背景色 */
--pe-code-block-bg-color: #f5f5f5;
/* 代码块复制按钮字体颜色 */
--pe-copy-code-color: #fff;
/* 代码块复制按钮背景色 */
--pe-copy-code-bg-color: #979797;
--pe-scrollbar-bg-color: rgb(163, 163, 165);
--pe-scrollbar-hover-bg-color: rgb(113, 113, 117);
--copy-btn-hover-color: #4489f9;
--code-color: #ff8a8a;
}
.dark {
/* 代码块标题色 */
--pe-code-block-header-color: var(--primary);
/* 代码块标题背景色 */
--pe-code-block-header-bg-color: #20252B;
/* 代码块文本颜色 */
--pe-code-block-color: rgba(255, 255, 255, 0.7);
/* 代码块背景色 */
--pe-code-block-bg-color: #272C34;
--pe-copy-code-color: rgba(255, 255, 255, 0.7);
--pe-copy-code-bg-color: #414244;
--pe-scrollbar-bg-color: rgb(113, 113, 117);
--pe-scrollbar-hover-bg-color: rgb(163, 163, 165);
--copy-btn-hover-color: #b3d0ff;
}
.post-content a:hover {
color: var(--copy-btn-hover-color);
}
/* 代码样式 */
.post-content code {
margin: unset;
padding: .3rem .4rem;
line-height: 1.5;
background: var(--code-bg);
border-radius: .5rem;
font-size: 0.875em;
font-family: Consolas, sans-serif;
color: var(--code-color);
}
/* 代码块样式 */
.pe-code-block-wrap {
border-radius: var(--radius);
margin: var(--content-gap) auto;
background-color: var(--pe-code-block-header-bg-color);
font-family: Consolas, sans-serif;
overflow: hidden;
}
.pe-code-block-header {
display: flex;
width: 100%;
align-items: center;
color: var(--pe-code-block-header-color);
justify-content: space-between;
padding: .4rem 1rem;
font-size: 0.875rem;
}
.pe-code-block-header-left {
text-align: left;
display: flex;
align-items: baseline;
gap: .2rem;
}
.pe-code-block-header-center {
text-align: center;
}
.pe-code-block-header-right {
line-height: 1rem;
text-align: right;
width: 2rem;
display: flex;
justify-content: flex-end;
}
.post-content .highlight:not(table) {
margin: unset;
background: var(--pe-code-block-bg-color) !important;
border-radius: unset;
}
.post-content pre code {
background-color: var(--pe-code-block-bg-color) !important;
font-size: 0.875rem;
color: var(--pe-code-block-color);
border-radius: unset;
}
.pe-icon {
width: 1.6rem;
height: 1.6rem;
}
.copy-code:hover {
background: var(--pe-primary-hover-color);
}
.chroma .lnt {
padding: 0 0 0 1.2rem !important;
}
/* 滚动条 */
.post-content :not(table) ::-webkit-scrollbar-thumb {
border: .2rem solid var(--pe-code-block-bg-color);
background: var(--pe-scrollbar-bg-color);
}
.post-content :not(table) ::-webkit-scrollbar-thumb:hover {
background: var(--pe-scrollbar-hover-bg-color);
}
.pe-code-details-content::-webkit-scrollbar {
width: .8rem;
}
.pe-code-details-content::-webkit-scrollbar-track {
background: var(--pe-code-block-bg-color); /* Background of the scrollbar track */
}
.pe-code-details-content::-webkit-scrollbar-thumb {
border: .2rem solid var(--pe-code-block-bg-color);
background: var(--pe-scrollbar-bg-color);
}
.pe-code-details-content::-webkit-scrollbar-thumb:hover {
background: var(--pe-scrollbar-hover-bg-color);
}
.pe-code-details-content::-webkit-scrollbar-corner {
background: var(--pe-code-block-bg-color);
}
table.lntable {
overflow-x: unset;
}
.pe-code-block-container pre {
margin: unset;
}
.pe-code-details .pe-code-details-summary:hover {
cursor: pointer;
}
.pe-code-details i.pe-code-details-icon {
color: var(--content);
-webkit-transition: transform 0.2s ease;
-moz-transition: transform 0.2s ease;
-o-transition: transform 0.2s ease;
transition: transform 0.2s ease;
}
.dark .pe-code-details i.pe-code-details-icon {
color: var(--content);
}
.pe-code-details .pe-code-details-content {
max-height: 0;
overflow-y: hidden;
-webkit-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
-moz-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
-o-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
}
.pe-code-details.open i.pe-code-details-icon {
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
.pe-code-details.open .pe-code-details-content {
max-height: 80vh;
-webkit-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
-moz-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
-o-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
}
.pe-code-details.scrollable .pe-code-details-content{
overflow: auto;
}
.scrollable {
overflow: auto;
}
.pe-code-details .fa-chevron-right:before {
content: "\f105";
}
.pe-code-details .fa-ellipsis-h:before {
content: "\f141";
}
.pe-code-details.open .fa-ellipsis-h:before {
content: "";
}
.pe-code-details .pe-code-copy-button {
display: none;
}
.pe-code-details.open .pe-code-copy-button {
display: inherit;
}
.pe-code-copy-button:hover {
color: var(--copy-btn-hover-color);
}
最后,在 footer.html
或 extend_footer.html
中引入脚本实现折叠动画
html
<script>
document.querySelectorAll('pre > code').forEach((codeBlock) => {
const codeBlockWrap = codeBlock.closest('.pe-code-block-wrap')
const copyButton = codeBlockWrap.querySelector('button')
function copyingDone() {
copyButton.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="pe-icon"><path fill-rule="evenodd" clip-rule="evenodd" d="M18.0633 5.67375C18.5196 5.98487 18.6374 6.607 18.3262 7.06331L10.8262 18.0633C10.6585 18.3093 10.3898 18.4678 10.0934 18.4956C9.79688 18.5234 9.50345 18.4176 9.29289 18.2071L4.79289 13.7071C4.40237 13.3166 4.40237 12.6834 4.79289 12.2929C5.18342 11.9023 5.81658 11.9023 6.20711 12.2929L9.85368 15.9394L16.6738 5.93664C16.9849 5.48033 17.607 5.36263 18.0633 5.67375Z" fill="currentColor"></path></svg>'
setTimeout(() => {
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="pe-icon"><path fill="currentColor" fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z" clip-rule="evenodd"></path></svg>'
}, 2000);
}
copyButton.addEventListener('click', (cb) => {
// 防止触发 details-summary 点击事件
cb.stopPropagation();
if ('clipboard' in navigator) {
navigator.clipboard.writeText(codeBlock.textContent).then(() => {
copyingDone();
})
return;
}
const range = document.createRange();
range.selectNodeContents(codeBlock);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
try {
document.execCommand('copy');
copyingDone();
} catch (e) {
}
selection.removeRange(range);
});
});
let peCodeDetails = document.getElementsByClassName('pe-code-details')
for (let element of peCodeDetails) {
const peCodeSummary = element.getElementsByClassName('pe-code-details-summary')[0];
if (peCodeSummary) {
peCodeSummary.addEventListener('click', () => {
if (element.classList.contains('open')) {
element.classList.remove('open');
element.classList.remove('scrollable');
} else {
element.classList.add('open');
setTimeout(() => {
element.classList.add('scrollable');
}, 800);
}
}, false);
}
}
</script>
ShortCode拓展
Hugo可以通过添加ShortCode来拓展Markdown的渲染能力,类似于组件。 所有的ShortCode定义都可以按照在 layouts/shortcode
中添加 html 文件和在 assets/css/extended
下新增 css 文件的方式来实现。如需JS则在 static/js
中添加js并在html中引入。
使用方式:
markdown
<shortcode>
content
</shortcode>
为了方便管理,我新建了一个shortcode目录用于存放shortcode的css代码,需要在 extend_head.html
中引入。引入代码如下:
html
{{ $styles := resources.Match "css/extended/shortcode/*.css" }}
{{ $style := $styles | resources.Concat "assets/css/shortcode.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}" integrity="{{ $style.Data.Integrity }}" crossorigin="anonymous">
admonition
代码来自 YazidLee/hugo-backup 仅略作修改
添加以下文件
html
{{- $inner := .Inner | .Page.RenderString -}}
{{- $iconMap := dict "note" "fas fa-pencil-alt fa-fw" -}}
{{- $iconMap = dict "abstract" "fas fa-list-ul fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "info" "fas fa-info-circle fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "tip" "fas fa-lightbulb fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "success" "fas fa-check-circle fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "question" "fas fa-question-circle fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "warning" "fas fa-exclamation-triangle fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "failure" "fas fa-times-circle fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "danger" "fas fa-skull-crossbones fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "bug" "fas fa-bug fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "example" "fas fa-list-ol fa-fw" | merge $iconMap -}}
{{- $iconMap = dict "quote" "fas fa-quote-right fa-fw" | merge $iconMap -}}
{{- $iconDetails := "fas fa-angle-right fa-fw" -}}
{{- if .IsNamedParams -}}
{{- $type := .Get "type" | default "note" -}}
<div class="details admonition {{ $type }}{{ if .Get `open` | ne false }} open{{ end }}">
<div class="details-summary admonition-title">
<i class="icon {{ index $iconMap $type | default (index $iconMap "note") }}"></i>{{ .Get "title" | default (T $type) }}<i class="details-icon {{ $iconDetails }}"></i>
</div>
<div class="details-content">
<div class="admonition-content">
{{- $inner -}}
</div>
</div>
</div>
{{- else -}}
{{- $type := .Get 0 | default "note" -}}
<div class="details admonition {{ $type }}{{ if .Get 2 | ne false }} open{{ end }}">
<div class="details-summary admonition-title">
<i class="icon {{ index $iconMap $type | default (index $iconMap "note") }}"></i>{{ .Get 1 | default (T $type) }}<i class="details-icon {{ $iconDetails }}"></i>
</div>
<div class="details-content">
<div class="admonition-content">
{{- $inner -}}
</div>
</div>
</div>
{{- end -}}
css
.admonition {
position: relative;
margin: 1rem 0;
padding: 0 0.75rem;
background-color: rgba(68, 138, 255, 0.1);
border-left: 0.25rem solid #448aff;
border-radius: var(--radius);
overflow: auto;
}
.admonition .admonition-title {
font-weight: bold;
margin: 0 -0.75rem;
padding: 0.25rem 1.8rem;
border-bottom: 1px solid rgba(68, 138, 255, 0.1);
background-color: rgba(68, 138, 255, 0.25);
}
.admonition.open .admonition-title {
background-color: rgba(68, 138, 255, 0.1);
}
.admonition .admonition-content {
padding: 0.5rem 0;
}
.admonition i.icon {
font-size: 0.85rem;
color: #448aff;
position: absolute;
top: 0.8rem;
left: 0.4rem;
}
.admonition i.details-icon {
position: absolute;
top: 0.6rem;
right: 0.3rem;
}
.admonition.note {
border-left-color: #448aff;
}
.admonition.note i.icon {
color: #448aff;
}
.admonition.abstract {
border-left-color: #00b0ff;
}
.admonition.abstract i.icon {
color: #00b0ff;
}
.admonition.info {
border-left-color: #00b8d4;
}
.admonition.info i.icon {
color: #00b8d4;
}
.admonition.tip {
border-left-color: #00bfa5;
}
.admonition.tip i.icon {
color: #00bfa5;
}
.admonition.success {
border-left-color: #00c853;
}
.admonition.success i.icon {
color: #00c853;
}
.admonition.question {
border-left-color: #64dd17;
}
.admonition.question i.icon {
color: #64dd17;
}
.admonition.warning {
border-left-color: #ff9100;
}
.admonition.warning i.icon {
color: #ff9100;
}
.admonition.failure {
border-left-color: #ff5252;
}
.admonition.failure i.icon {
color: #ff5252;
}
.admonition.danger {
border-left-color: #ff1744;
}
.admonition.danger i.icon {
color: #ff1744;
}
.admonition.bug {
border-left-color: #f50057;
}
.admonition.bug i.icon {
color: #f50057;
}
.admonition.example {
border-left-color: #651fff;
}
.admonition.example i.icon {
color: #651fff;
}
.admonition.quote {
border-left-color: #9e9e9e;
}
.admonition.quote i.icon {
color: #9e9e9e;
}
.admonition.note {
background-color: rgba(68, 138, 255, 0.1);
}
.admonition.note .admonition-title {
border-bottom-color: rgba(68, 138, 255, 0.1);
background-color: rgba(68, 138, 255, 0.25);
}
.admonition.note.open .admonition-title {
background-color: rgba(68, 138, 255, 0.1);
}
.admonition.abstract {
background-color: rgba(0, 176, 255, 0.1);
}
.admonition.abstract .admonition-title {
border-bottom-color: rgba(0, 176, 255, 0.1);
background-color: rgba(0, 176, 255, 0.25);
}
.admonition.abstract.open .admonition-title {
background-color: rgba(0, 176, 255, 0.1);
}
.admonition.info {
background-color: rgba(0, 184, 212, 0.1);
}
.admonition.info .admonition-title {
border-bottom-color: rgba(0, 184, 212, 0.1);
background-color: rgba(0, 184, 212, 0.25);
}
.admonition.info.open .admonition-title {
background-color: rgba(0, 184, 212, 0.1);
}
.admonition.tip {
background-color: rgba(0, 191, 165, 0.1);
}
.admonition.tip .admonition-title {
border-bottom-color: rgba(0, 191, 165, 0.1);
background-color: rgba(0, 191, 165, 0.25);
}
.admonition.tip.open .admonition-title {
background-color: rgba(0, 191, 165, 0.1);
}
.admonition.success {
background-color: rgba(0, 200, 83, 0.1);
}
.admonition.success .admonition-title {
border-bottom-color: rgba(0, 200, 83, 0.1);
background-color: rgba(0, 200, 83, 0.25);
}
.admonition.success.open .admonition-title {
background-color: rgba(0, 200, 83, 0.1);
}
.admonition.question {
background-color: rgba(100, 221, 23, 0.1);
}
.admonition.question .admonition-title {
border-bottom-color: rgba(100, 221, 23, 0.1);
background-color: rgba(100, 221, 23, 0.25);
}
.admonition.question.open .admonition-title {
background-color: rgba(100, 221, 23, 0.1);
}
.admonition.warning {
background-color: rgba(255, 145, 0, 0.1);
}
.admonition.warning .admonition-title {
border-bottom-color: rgba(255, 145, 0, 0.1);
background-color: rgba(255, 145, 0, 0.25);
}
.admonition.warning.open .admonition-title {
background-color: rgba(255, 145, 0, 0.1);
}
.admonition.failure {
background-color: rgba(255, 82, 82, 0.1);
}
.admonition.failure .admonition-title {
border-bottom-color: rgba(255, 82, 82, 0.1);
background-color: rgba(255, 82, 82, 0.25);
}
.admonition.failure.open .admonition-title {
background-color: rgba(255, 82, 82, 0.1);
}
.admonition.danger {
background-color: rgba(255, 23, 68, 0.1);
}
.admonition.danger .admonition-title {
border-bottom-color: rgba(255, 23, 68, 0.1);
background-color: rgba(255, 23, 68, 0.25);
}
.admonition.danger.open .admonition-title {
background-color: rgba(255, 23, 68, 0.1);
}
.admonition.bug {
background-color: rgba(245, 0, 87, 0.1);
}
.admonition.bug .admonition-title {
border-bottom-color: rgba(245, 0, 87, 0.1);
background-color: rgba(245, 0, 87, 0.25);
}
.admonition.bug.open .admonition-title {
background-color: rgba(245, 0, 87, 0.1);
}
.admonition.example {
background-color: rgba(101, 31, 255, 0.1);
}
.admonition.example .admonition-title {
border-bottom-color: rgba(101, 31, 255, 0.1);
background-color: rgba(101, 31, 255, 0.25);
}
.admonition.example.open .admonition-title {
background-color: rgba(101, 31, 255, 0.1);
}
.admonition.quote {
background-color: rgba(60, 60, 60, .1);
}
.admonition.quote .admonition-title {
border-bottom-color: rgba(60, 60, 60, 0.1);
background-color: rgba(60, 60, 60, 0.25);
}
.admonition.quote.open .admonition-title {
background-color: rgba(60, 60, 60, 0.1);
}
.admonition:last-child {
margin-bottom: 0.75rem;
}
css
.details .details-summary:hover {
cursor: pointer;
}
.details .details-content {
max-height: 0;
overflow-y: hidden;
-webkit-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
-moz-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
-o-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
}
.details.open .details-content {
max-height: 1200rem;
-webkit-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
-moz-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
-o-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
}
.details .details-icon {
color: var(--content);
-webkit-transition: transform 0.2s ease;
-moz-transition: transform 0.2s ease;
-o-transition: transform 0.2s ease;
transition: transform 0.2s ease;
}
[class=dark] .details .details-icon {
color: var(--content);
}
.details.open .details-icon {
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
最后,在 footer.html
或 extend_footer.html
中引入脚本实现折叠动画
html
<script>
let details = document.getElementsByClassName('details')
details = details || [];
for (let i = 0; i < details.length; i++) {
let element = details[i]
const summary = element.getElementsByClassName('details-summary')[0];
if (summary) {
summary.addEventListener('click', () => {
element.classList.toggle('open');
}, false);
}
}
</script>
效果展示: {{< admonition type=tip title="This is a tip" open=true >}} 一个 技巧 横幅 {{< /admonition >}}
界面显示优化
配置默认语言为中文
在 hugo.html
文件中添加配置 defaultContentLanguage: zh
可以汉化大部分主题的内容
日期格式显示优化
在 hugo.html
文件中的Param节点下添加配置 DateFormat: "2006-01-02"
(go的时间配置必须要用这个日期,yyyy-MM-dd不行)
标签页添加词云
参考资料: blog.xlap.top/post/tech/w... 感谢开源作者和大佬代码,我直接抄了
- 下载
wordcloud2.js
拷贝到static/js/
目录下,下载地址 - 修改
head.html
引入js文件,新增以下代码:
html
{{- if eq .Section "tags"}}
{{/* 标签云 */}}
<link rel="stylesheet" href="/css/word-cloud.css"\>
<script src="/js/wordcloud2.js"></script>
{{- end }}
- 创建
staic/css/word-cloud.css
文件输入以下代码:
css
.word-color:nth-child(7n + 1) {
color: rgb(202, 110, 255);
}
.word-color:nth-child(7n + 2) {
color: rgb(83, 110, 255);
}
.word-color:nth-child(7n + 3) {
color: rgb(143, 253, 241);
}
.word-color:nth-child(7n + 4) {
color: rgb(183, 255, 112);
}
.word-color:nth-child(7n + 5) {
color: rgb(255, 212, 126);
}
.word-color:nth-child(7n + 6) {
color: rgb(248, 140, 131);
}
.word-color:nth-child(7n + 7) {
color: rgb(104, 160, 255);
}
@keyframes word {
0% {
opacity: 0.5;
}
3% {
opacity: 1;
}
9% {
opacity: 1;
}
12% {
opacity: 0.5;
}
100% {
opacity: 0.5;
}
}
.word-animate {
animation-name: word;
animation-duration: 20s;
animation-iteration-count: infinite;
will-change: opacity;
opacity: 0.5;
}
.word-animate:nth-child(7n + 1) {
animation-delay: 0s;
}
.word-animate:nth-child(7n + 2) {
animation-delay: 3s;
}
.word-animate:nth-child(7n + 3) {
animation-delay: 6s;
}
.word-animate:nth-child(7n + 4) {
animation-delay: 9s;
}
.word-animate:nth-child(7n + 5) {
animation-delay: 12s;
}
.word-animate:nth-child(7n + 6) {
animation-delay: 15s;
}
.word-animate:nth-child(7n + 7) {
animation-delay: 18s;
}
- 创建
layouts/_default/terms.html
输入以下代码
html
{{- define "main" }}
{{- if .Title }}
<header class="page-header">
<h1>{{ .Title }}</h1>
{{- if .Description }}
<div class="post-description">
{{ .Description }}
</div>
{{- end }}
</header>
{{- end }}
<ul class="terms-tags">
<div id="sourrounding_div" style="width:100%;height:100%;min-height: 500px;">
<div id="tag-canvas"></div>
</div>
<script src="/js/wordcloud2.js"></script>
{{- range $key, $value := .Data.Terms.Alphabetical }}
{{ if eq "" ($.Scratch.Get "tagsMap") }}
{{ $.Scratch.Set "tagsMap" (slice (dict .Name .Count)) }}
{{ else }}
{{ $.Scratch.Add "tagsMap" (slice (dict .Name .Count)) }}
{{ end }}
{{- end }}
{{ $result := ($.Scratch.Get "tagsMap")}}
<span id="tag-temp" style="display:none">{{$result | jsonify }}</span>
<script>
//因为前期每个标签值比较小,帮X一个系数
var XISHU = 20;
//为了动态宽度
var div = document.querySelector("#sourrounding_div");
var canvas = document.querySelector("#tag-canvas");
canvas.style.width = div.offsetWidth + 'px';
canvas.style.height = div.offsetHeight + 'px';
var wordFreqData = document.querySelector("#tag-temp").innerHTML;
var jsonObj = JSON.parse(wordFreqData);
var arr = []
jsonObj.forEach(element => {
var key = Object.keys(element);
var itemArr = [key[0], element[key] * XISHU];
arr.push(itemArr);
});
//获取当前是暗色还是浅色
var isDark = document.body.className.includes("dark");
WordCloud(canvas, {
"list": arr,//或者[['各位观众',45],['词云', 21],['来啦!!!',13]],只要格式满足这样都可以
"shape": "cardioid", //形状 circle (default), cardioid (心型), diamond, square, triangle-forward, triangle, pentagon, and star.
"gridSize": 20, // 密集程度 数字越小越密集
"weightFactor": 1, // 字体大小=原始大小*weightFactor
"fontWeight": 'normal', //字体粗细
"fontFamily": 'Times, serif', // 字体
"color": isDark ? 'random-light' : 'random-dark', // 字体颜色 'random-dark' 或者 'random-light'
"backgroundColor": 'none', // 背景颜色
"classes": "tag-cloud-item word-color", //用于点击事件
});
canvas.addEventListener('wordcloudstop', function (e) {
//动画
setTimeout(() => {
var els = document.querySelectorAll(".word-color");
Array.from(els).forEach((el) => {
console.log('动画', el)
el.classList.add("word-animate")
})
}, 2000);
//点击
document.querySelectorAll('.tag-cloud-item').forEach(function (element) {
const text = element.innerHTML;
element.innerHTML = `<a href="/tags/${text}" style="color: inherit;">${text}</a>`;
});
});
</script>
</ul>
{{- end }}{{/* end main */ -}}
参数可以自行修改,可以关注 shape
和 gridSize
等属性
url管理
在 hugo.yaml
中添加 permalinks.posts: "/:year/:month/:day/:slug/"
在 archetypes/default.md
中添加 slug:
在每一篇博文中,自己管理slug
为文章添加过期提示
- 修改
single.html
文件中,在post-meta后面新增一个div块来展示该信息,代码如下:
html
{{- if not (.Param "hideMeta") }}
<div class="post-meta">
{{- partial "post_meta.html" . -}}
{{- partial "translation_list.html" . -}}
{{- partial "edit_post.html" . -}}
{{- partial "post_canonical.html" . -}}
</div>
{{- end }}
<!-- 添加提示框逻辑 -->
{{- $lastmod := .Lastmod }}
{{- $now := now }}
{{- $duration := $now.Sub $lastmod }}
{{- $daysAgo := $duration.Hours }}
{{- if ge $daysAgo (math.Mul 180 24) }}
{{- if eq site.Params.defaultTheme `dark` -}}
{{- $isDark := ".dark" }}
{{- end -}}
<div class="post-age-hint">
这篇文章最后更新已经超过180天了,内容可能已经过时。
</div>
- 为该div块定制样式,在
assets/css/extended
目录下新增custom.css
文件,输入以下代码,颜色可以随自己喜欢调整
css
:root {
--post-age-hint-bg-color: #d4d4d4ac;
--post-age-hint-color: #26b3fe;
}
.dark {
--post-age-hint-bg-color: #6d6c6cac;
--post-age-hint-color: #ff7723;
}
.post-age-hint {
background-color: var(--post-age-hint-bg-color);
/* 深灰色背景 */
color: var(--post-age-hint-color);
/* 浅灰色文字 */
padding: 10px;
border-left: 3px solid #ff6666;
/* 红色边框,醒目但不刺眼 */
margin: 20px 0;
border-radius: 5px;
/* 圆角边框 */
}
/* 添加一个过渡效果,鼠标悬停时改变边框颜色 */
.post-age-hint:hover {
border-color: #ff9999;
}
这段代码中,定义了两个变量用于适配不同主题,默认的白色主题,会使用root中的两个颜色,当切换到dark时,会使用.dark中定义的颜色
驾驶体验
Hugo作为建站博客工具的时候,博文的载体是 markdown
,而 markdown
本身是一个纯文本的标记语言,在Hugo工具提供的命令中,会转换成静态的网页文件来提供更加优质的阅读体验。所以,在理论上,选用任何的文本编辑器,其实都可以作为博客的写作工具。
然而,在真实使用的情况下,我们需要先在命令行中,输入 hugo new posts/xxx.md
命令来创建一篇文章,然后打开对应的文本编辑器来编辑文章,还需要在命令中使用 hugo serve -D
来看一下显示效果,如果用到图片,还需要手动管理图片的地址来保证本地和发布之后保持一致,整个过程就会显得非常的繁琐。
基于以上我博客的编写流程,提炼了几个痛点和几点期望
- 创建文章需要输入命令,我希望可以通过图形化界面快速的创建文章
- 博客图片的管理,利用图床进行图片
本文将会围绕这两个痛点,对写作体验进行逐步的优化。我计划使用Obsidian作为我的写作工具。以下将是我计划中的写作及发布流程(配合自动发布):
如果你希望跟我拥有一样的配置,那么需要提前准备好以下软件或账号:
- 一个基于Hugo构建的笔记目录
- Obsidian : 一款舒适的Markdown编辑器
- PicList: 一款基于PicGo二次开发的图床管理工具
- 腾讯云COS服务: 请自行前往腾讯云开启(有免费额度,超过之后需要付费)
插件&软件
以下插件均指代Obsidian插件市场中的插件,软件为系统软件,均需要自己安装。 对于Obsidian插件需要关闭安全模式,来开启第三方插件,以下插件均可以通过插件市场下载,插件在Github中,如遇到网络问题请自行解决。 安装完OB插件记得重启
功能插件
以下插件均为必须安装的插件,用于实现前文提到的功能。
Shell Command
Shell Command是一款可以在Obsidian中运行操作系统命令的插件,由于hugo和Git的操作之前都是基于命令行手敲的,所以我选用了他作为实现层的插件,在Shell Command中包装好常用的命令,然后通过其他插件来进行更近一步的封装。
Commander
Shell Command是一款可以将命令设置为图标的插件,用于将Hugo创建博客的命令可视化
Git
用于可视化Git操作
Hugo Preview
用于在本地进行Hugo网站预览,可以实时预览,非常好用,免去了打开命令行输入hugo server -D
命令的麻烦(实际上这个按钮代替你执行了这个命令),如果有双屏的话,可以一个屏幕预览一个屏幕书写,非常好用。
Image Auto Upload
文件主动上传到图床的工具,需要配合PicGo来使用。
美化&操作效率插件
以下插件为选装插件,用于提升外观和使用效率。有很多图片相关的插件,因为Obsidian的图片管理实在令人感到很苦闷。
主题:Board
单纯的记录以下我现在正在用的主题插件
Hide Folders
Obsidian只是一个单纯的Mardown编辑器,很多代码相关的文件不会显示,所以目录下会有很多平时写作时完全不需要的目录,可以通过Hide Folders插件进行隐藏。
Clear Unused Image
后来我发现不需要安装,因为上传插件中可以开启图片上传后移除
不推荐Git使用不熟练的同学安装,如果要安装,务必理解配置的含义,或者完全照抄我的。 这是一个删除未使用图片的插件,我按照我的使用习惯,进行编译时,会把图片放在本地,等上传图床后,进行删除。
Paste Image Rename
一个用于将截图添加到文章中进行重命名的插件
Attachment Manager
OB的附件管理软件,可以将一个笔记下的附件(主要是图片)放到单独的文件夹下,我会将这个路径映射到图床上,图片的管理和会方便很多,可以配合 Paste Image Rename 使用,相当丝滑,强烈推荐。
Code Styler
用于实现代码输出美化的插件,主要是用来折叠代码。
软件
PicGO
PicGo是一款图床管理软件,支持主流的多种云服务图像存储或图床服务。
PicList
PicList是基于PicGo二次开发的图床管理软件,我主要是为了方便进行文件的管理(带删除和管理相当的方便)。最终选用了PicList。
需要在PicList中添加一个名为 rename-file
的插件,来配合OB附件管理插件来进行图片的分目录管理
配置
非功能插件配置
Hide Folders
需要在Hide Folder的配置界面,进行隐藏文件夹进行配置。(没找到白名单模式,不知道后续作者会不会更新一个白名单模式)。在Folders to hide 中添加 。配置完成之后只有archetypes和content两个文件夹目录了,瞬间就变得很清爽,可以更加专注博客的编辑而不被hugo的项目文件影响。
arduino
assets
data
i18n
layouts
public
resources
static
themes
Paste Image Rename
一个非常好用的插件,不需要配置,在截完图粘贴进Obsidian之后就会有一个对话框让你输入新的文件名,第一次会提示是否需要更新文章中的链接,直接点击以后一直更新就好了。
Attachment Manager
附件管理插件,我没有进行其他配置,安装完启用即可,真正的开箱即用。
Code Sytler
设置,选择 CodeBlock Styling后,选择 Codeblock Header,选择 Display Header Language Tags 为Always
1. 实现图形化新建文章
Hugo的文章创建需要使用 Commander
命令来进行创建,为了让Hugo给文章进行初始化的设置(可能还有其他的配置文件修改,根据我自己的使用经验,在posts目录下创建的文章可以被编译,但对于新增的Layout直接新建文件会无法编译)。所以,我认为还是通过在图像界面上新增一个功能来代替我们输入命令会更合适一些。
需要用到的插件
- Shell Command:用于将
hugo new
命令封装成Obsidian的命令 - Commander:用于将封装好的Obsidian命令添加到Obsidian的界面上
Shell Command
使用前确保hugo相关命令可以被命令行程序调用,即hugo.exe需要在环境变量中,可以通过打开cmd输入 hugo version
进行尝试。
-
打开Shell Command的配置界面,新增一条Shell Command 在命令界面中输入
hugo new posts/{{_filename}}.md
这是我们需要执行的新增文档命令,其中{{_filename}}
是一个变量,下一步中会配置接收变量的方式 -
打开Shell Command的配置界面,新增一条Precations。点击new promt按钮,可以进入图片中的编辑页面,名称可以随意取,最关键是的需要绑定到
{{_filename}}
变量,在Target Variable中新建这个变量即可 -
在Shell Command命令的配置面板,点击命令的小齿轮按钮,打开详细设置,连接 proactions和命令,可以在通用设置中添加一个别名方便寻找
配置完成后,可按 Ctrl + P
唤出Obsidian的操作面板进行尝试
Commander
- 打开Commander的配置面板,在左侧栏目中,新增一个按钮 具体步骤为,点击添加命令,选中 Shell Command创建的命令,输入界面中按钮名称 配置完成后,可以在左侧看到一个新的按钮
2. 图床配置
博客的图片管理一直是让人头疼的事情,综合费用和便捷程度考虑,我选用腾讯云的是COS作为图床服务,免费的额度足够日常使用,因为我平时写文基本都是打开源码模式(当然,就算不用源码模式写文也用不完免费额度,主要还是在博客生产环境的浏览量),当然也可以选用其他的图床服务,配置的不同仅仅在于PicGo的目标端。
PicList软件配置
软件下载安装完成之后,配置腾讯云COS的链接信息,并设置为默认图床。
需要准备以下内容:
- SecretId:在腾讯云,头像Hover下的菜单-访问管理-API管理中创建
- SecretKey: 在腾讯云,头像Hover下的菜单-访问管理-API管理中创建,只出现一次,下载下来记住了
- Bucket:在腾讯云-控制台-对象存储-存储桶列表
- AppId:在腾讯云,头像Hover下的菜单-访问管理-API管理中创建
- 存储区域:在腾讯云-控制台-对象存储-存储桶列表
将内容配置到PicList中,并设置为默认图床,可以上传一个图片进行测试。
然后是可选的插件配置,在PicList的界面中,打开菜单插件,搜索 rename-file插件,安装并进行配置。配置文件格式路径为 /{localFolder:1}/{origin}
,在我实际的使用过程中,开启附件管理后,图床上的位置为 /${blogfile}/content/posts/xx.md_Attachements/xx.png
。完全符合我的预期,图片可以根据文档分类存放,在后期的管理和博客更新过程中也会非常方便。
Iamge Auto Upload插件配置
可选 可以开启可选项,上传后删除源文件。其他都不需要配置
Commander新增一键上传
在页首栏新增一个按钮 最终会在编辑器的上方,一本阅读模式按钮(一本书) 边上出现一个新按钮,用于一件上传本文中的所有图片到图床。
3. Hugo Preview
安装好Hugo Preview插件后。会在右下角出现一个Go的图标(蓝色),点击之后就会出现Hugo站点的预览。(双屏食用更佳)
FAQ: 已知问题
记录本人在配置过程中遇到的问题及解决方案
1. Git跟踪文件异常
- 现象:在Obsidian中,文件没有修改,Git跟踪的文件中却提示被修改哦,打开diff看发现也没有区别
- 原因:Windows系统下Git开启了换行符自动转换,Ob转换为LF,而Git转换为CRLF
- 解决方案:在仓库目录下输入命令
git config core.autocrlf input
所有换行符统一转换为LF
2. Git无法使用&&Hugo Preview无法使用
- 现象: Git使用时,提示报错obsidian cannot run git command,hugo preview 提示报错
- 原因: 无法调用到Git命令
- 解决方案:
- (推荐方案) Git命令未加入环境变量,编辑系统环境变量,将 Git的bin目录加入系统环境变量
- 在Obsidian的Git插件中配置Git的地址为git.exe的绝对路径