1 创建站点及主题下载#
hugo new site blog.sopuy.com
cd blog.sopuy.com
git init
git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
git submodule update --init --recursive # needed when you reclone your repo (submodules may not get cloned automatically)
2 网站配置#
- 配置原则:
- 目前使用了 PaperMod 主题的 profileMode 模式;
2.1 实现 Home/Tags/Categories/Posts#
- 属于 PaperMod 已经默认实现的一个页面,只要在菜单中配置中增加一下即可;
menu:
main:
- name: "🏠 Home"
identifier: "home"
url: "/"
weight: 1
- name: "📚 Posts"
identifier: "posts"
url: "/posts"
weight: 2
- name: "🔖 Tags"
identifier: "tags"
url: "/tags"
weight: 3
- name: "🧩 Categories"
identifier: "categories"
url: "/categories"
weight: 4
2.2 实现归档功能#
- Archive 即博客中的归档、时间线功能,用于按照时间对博文分类管理。
- 在 <your_web_site>/content/ 目录下新建 archive.md,并添加以下模板内容:
---
title: "🕓 Archives"
layout: "archives"
url: "/archives/"
summary: archives
---
- 修改网站配置文件,添加一个归档的菜单,即可通过点击菜单栏上归档按钮进入时间线:
menu:
main:
- name: "🕓 Archives"
identifier: "archives"
url: "/archives"
weight: 5
2.3 实现搜索功能#
- 搜索也是 PaperMod 官方支持的模块,支持对博文内容、标题、关键字等进行索引。
- 在 <your_web_site>/content/ 目录下新建 search.md,并添加以下模板内容:
---
title: "Search" # in any language you want
layout: "search" # necessary for search
# url: "/archive"
# description: "Description for Search"
summary: "search"
placeholder: "placeholder text in search input box"
---
- 修改网站配置文件,添加启用搜索所需的配置信息和搜索的菜单按钮:
# activate search
outputs:
home:
- HTML
- RSS
- JSON # necessary for search
menu:
main:
- name: "🔍 Search"
identifier: "search"
url: "/search"
weight: 100
2.4 文章页面 TOC 使用侧边栏#
- 在 PaperMod 中,目录的默认行为是在文章前展示,在阅读过程中无法利用其帮助定位或者精确跳转到某一部分,侧边目录能够很好解决上述痛点。此外,阅读进度百分比也能够帮助读者定位阅读位置,还能让网页显得更灵动一点。
- 复制主题目录下
layouts/partials/toc.html
至 <your_hugo_site>/layouts/partials/toc.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 }}
- 添加 css 样式的代码,创建 <your_hugo_site>/assets/css/extended/toc.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);
}
3 TODO#
- 搜索优化
- 添加评论功能
- 文章列表也图片显示
- 文章内的代码复制按钮
4 参考资料#