今天有网友邮件咨询我现在的主题 Eureka[1] 的一些自定义配置,他想参考一下。由于我的博客仓库是私有的,所以就写一篇文章简单整理一下。
Eureka[2] 是前段时间群友推荐给我的,纯白的朴素风格同时提供了暗色模式瞬间我就喜欢上了。将其 clone
到 Hugo 博客目录 themes/hugo-eureka
下,config.toml
中配置 theme = "hugo-eureka"
即可使用上该款主题。为了方便主题的更新,我将我所有自定义的模板都放在了 layouts
目录下。Hugo 会将主题目录和 layouts
目录下的文件进行合并,并优先使用 layouts
目录中的同名文件。这样之后我只需要单纯的更新 thtmes/hugo-eureka
目录即可。
相较于 Eureka 主题的默认首页,我个人还是比较喜欢传统博客的两栏布局,左侧显示模块列表,右侧显示文章列表,所以我需要自定义首页模板。拷贝以下内容创建 layouts/index.html
文件即可实现同款。
{{ define "main" }}
<div class="pl-scrollbar">
<div class="w-full max-w-screen-xl lg:px-4 xl:px-8 mx-auto">
<div class="max-w-screen-xl mx-auto" style="padding-top: 3rem">
<div class="bg-local bg-cover">
<img class="day" src="/banner-day.png" />
<img class="dark" src="/banner.png" />
</div>
</div>
<!-- <article class="mx-6 my-7">
<h1 class="font-bold text-3xl text-primary-text"></h1>
</article> -->
<div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12">
<div class="col-span-2 sidebar">
<div class="widget bg-secondary-bg rounded p-6">
<h2 class="widget-title">最新文章</h2>
<ul class="widget-list">
{{- $recent := default 5 .Site.Params.numberOfRecentPosts }}
{{- $posts := where (where .Site.RegularPages "Permalink" "!=" .Permalink) "Type" "in" .Site.Params.mainSections }}
{{- range first $recent $posts }}
<li>
<a href="{{ .RelPermalink }}" class="nav-link">{{ .Title }}</a>
</li>
{{- end }}
</ul>
</div>
<div class="widget bg-secondary-bg rounded p-6">
<h2 class="widget-title ">最近回复</h2>
<ul class="widget-list recentcomments">
{{ range first 10 (sort (partialCached "utils/get-comments" (dict "context" . "url" false)) "insertedAt" "desc")}}
<li class="recentcomments">
<a href="{{.Site.BaseURL}}{{ .url }}#{{.objectId}}">{{ .nick }}</a>:{{ .comment | safeHTML | truncate 22 }}
</li>
{{ end }}
</ul>
</div>
<div class="widget bg-secondary-bg rounded p-6">
<h2 class="widget-title">友情链接</h2>
<ul class="widget-list">
{{ range .Site.Menus.friends }}
<li>
<a href="{{ .URL }}">{{ .Name }}</a>
</li>
{{ end }}
</ul>
</div>
<div class="widget bg-secondary-bg rounded p-6">
<h2 class="widget-title">管理</h2>
<ul class="widget-list">
<li>
<a href="/admin"> 后台管理</a>
</li>
<li>
<a href="{{ .Site.Params.comment.waline.serverURL }}/ui"> 评论管理</a>
</li>
</ul>
</div>
</div>
<div class="col-span-2 lg:col-span-6 bg-secondary-bg rounded px-6 py-8">
<div class="bg-secondary-bg rounded overflow-hidden px-4 divide-y">
{{ range .Paginator.Pages }}
<div class="px-2 py-6">
{{ partial "horizontal_summary.html" . }}
</div>
{{ end }}
</div>
{{ template "_internal/pagination.html" . }}
</div>
</div>
</div>
</div>
{{ end }}
其中顶部还增加了一组暗色模式切换的横幅图片,添加以下 CSS 内容至 layouts/partials/custom_head.html
文件中,不存在的话需要新建。
.widget + .widget {
margin-top: 1rem;
}
.widget-title {
font-weight: bold;
margin-bottom: 1rem;
}
.widget-list li {
font-size: 0.9rem;
}
.bg-cover img {
opacity: 1;
transition: all .5s ease-in-out;
}
.bg-cover img.dark {
opacity: 0;
height: 0;
}
.dark .bg-cover img.day {
opacity: 0;
height: 0;
}
.dark .bg-cover img.dark {
opacity: 1;
height: auto;
}
左侧的模块中,评论是使用了本人自研的 Waline 评论系统并进行了一定的改造,具体可参见我之前的文章《静态博客如何高性能插入评论》[3]。当然也可以直接使用 Waline 自带的最近评论挂件[4]。
友情链接则是在 config.toml
中按照如下格式进行配置的。
[[menu.friends]]
name = "童欧巴博客"
url = "https://hungryturbo.com/"
weight = 20
[[menu.friends]]
identifier = "QingXu"
name = "QingXu"
url = "https://blog.qingxu.live"
weight = 19
[[menu.friends]]
identifier = "蜘蛛抱蛋"
name = "蜘蛛抱蛋"
url = "https://blog.zzbd.org/"
weight = 18
后台管理则是使用了 forestry[5] 提供的服务,它支持提供在线后台进行文章、页面和其它配置的管理。评论管理则是链接到了 Waline 服务的后台面板中。
Eureka 主题的文章 metadata 显示分为列表页和详情页两个,分别对应 post_metadata.html
和 post_metadata_full.html
两个文件。我们在 layouts/partials/
目录下新建这两个文件用来覆盖主题默认的文件。
<!--layouts/partials/post_metadata.html-->
<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>
{{- $slug := printf "/%s.html" .Slug}}
{{- $commentsData := (partialCached "utils/get-comments.html" .)}}
{{- $comments := slice }}
{{- range where $commentsData "url" "==" $slug}}
{{$comments = $comments | append .}}
{{- end}}
{{- $count := len $comments}}
<div class="mr-6 my-2">
<a href="{{ .Permalink }}#waline-comments" title="{{ .Title }}">
<i class="fas fa-comment mr-1"></i>
<span>{{- if gt $count 0}}{{$count}} 条评论{{else}}暂无评论{{end -}}</span>
</a>
</div>
<div class="mr-6 my-2">
<i class="fas fa-clock mr-1"></i>
<span>{{ i18n "readingTime" . }}</span>
</div>
{{ with .GetTerms "categories" }}
<div class="mr-6 my-2">
<i class="fas fa-folder mr-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 "series" }}
<div class="mr-6 my-2">
<i class="fas fa-th-list mr-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>
post_metadata.html
主要是增加了评论条数的显示,而 post_metadata_full.html
中还增加了 Markdown 原文链接的显示。关于如何生成 Markdown 原文链接,可以参考我之前的文章《Hugo 之旅》[6]。
<!--layouts/partials/post_metadata_full.html-->
<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>
{{$resp := getJSON "https://imnerd.vercel.app/comment?type=count&url=/" .Slug ".html" }}
<div class="mr-6 my-2">
<a href="{{ .Permalink }}#waline-comments" title="{{ .Title }}">
<i class="fas fa-comment mr-1"></i>
<span>{{- if gt $resp 0}}{{$resp}} 条评论{{else}}暂无评论{{end -}}</span>
</a>
</div>
{{ if eq .Type "posts" -}}
{{ with .OutputFormats.Get "MarkDown" -}}
<div class="mr-6 my-2">
<a href="{{ .Permalink }}">
<i class="fas fa-book mr-1"></i>
<span>阅读Markdown格式</span>
</a>
</div>
{{- end }}
{{ end }}
<div class="mr-6 my-2">
<a href="{{ .Permalink }}">
<i class="fas fa-pen mr-1"></i>
<span>{{ .WordCount }} 字</span>
</a>
</div>
<div class="mr-6 my-2">
<i class="fas fa-clock mr-1"></i>
<span>{{ i18n "readingTime" . }}</span>
</div>
{{ with .GetTerms "categories" }}
<div class="mr-6 my-2">
<i class="fas fa-folder mr-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 "series" }}
<div class="mr-6 my-2">
<i class="fas fa-th-list mr-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>
搜索也是博客比较重要的功能,为了方便我在顶部增加了搜索框。创建 layouts/partials/header.html
文件用来覆盖默认的头部模板。
<!--layouts/partials/header.html-->
<script>
let storageColorScheme = localStorage.getItem("lightDarkMode")
{{- if eq .Site.Params.colorScheme "light" }}
if ((storageColorScheme == 'Auto' && window.matchMedia("(prefers-color-scheme: dark)").matches) || storageColorScheme == "Dark") {
document.getElementsByTagName('html')[0].classList.add('dark')
}
{{- else if eq .Site.Params.colorScheme "dark" }}
if ((storageColorScheme == 'Auto' && window.matchMedia("(prefers-color-scheme: light)").matches) || storageColorScheme == "Light") {
document.getElementsByTagName('html')[0].classList.remove('dark')
}
{{- else }}
if (((storageColorScheme == 'Auto' || storageColorScheme == null) && window.matchMedia("(prefers-color-scheme: dark)").matches) || storageColorScheme == "Dark") {
document.getElementsByTagName('html')[0].classList.add('dark')
}
{{- end }}
</script>
<nav class="flex items-center justify-between flex-wrap p-4">
<a href="{{ "/" | relLangURL }}" class="mr-6 text-primary-text text-xl font-bold">{{ .Site.Title }}</a>
<button id="navbar-btn" class="md:hidden flex items-center px-3 py-2" aria-label="Open Navbar">
<i class="fas fa-bars"></i>
</button>
<div id="target"
class="hidden block md:flex md:flex-grow md:justify-between md:items-center w-full md:w-auto text-primary-text z-20">
<div class="text-sm md:flex-grow pb-4 md:pb-0 border-b md:border-b-0">
{{- $relPermalink := .RelPermalink }}
{{- range .Site.Menus.main }}
{{- $url := .URL | relLangURL }}
<a href="{{ $url }}"
class="block mt-4 md:inline-block md:mt-0 {{ if hasPrefix $relPermalink $url }} text-eureka {{ end }} hover:text-eureka mr-4">{{ .Name }}</a>
{{- end }}
</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="fas fa-search"></i>
</button>
</form>
</div>
</div>
<div class="relative pl-4 pt-4 md:pt-0">
<div class="cursor-pointer hover:text-eureka" id="lightDarkMode">
{{- if eq .Site.Params.colorScheme "dark" }}
<i class="fas fa-moon"></i>
{{- else if eq .Site.Params.colorScheme "light" }}
<i class="fas fa-sun"></i>
{{- else }}
<i class="fas fa-adjust"></i>
{{- end }}
</div>
<div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-30" id="is-open">
</div>
<div class="absolute flex flex-col left-0 md:left-auto right-auto md:right-0 hidden bg-secondary-bg w-48 rounded py-2 border border-tertiary-bg cursor-pointer z-40"
id='lightDarkOptions'>
<span class="px-4 py-1 hover:text-eureka">Light</span>
<span class="px-4 py-1 hover:text-eureka">Dark</span>
<span class="px-4 py-1 hover:text-eureka">Auto</span>
</div>
</div>
{{- if .IsTranslated }}
<div class="relative pt-4 pl-4 md:pt-0">
<div class="cursor-pointer hover:text-eureka" id="languageMode">
<i class="fas fa-globe"></i>
<span class="pl-1">{{ .Language.LanguageName }}</span>
</div>
<div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-30" id="is-open-lang">
</div>
<div class="absolute flex flex-col left-0 md:left-auto right-auto md:right-0 hidden bg-secondary-bg w-48 rounded py-2 border border-tertiary-bg cursor-pointer z-40"
id='languageOptions'>
<a class="px-4 py-1 hover:text-eureka" href="{{ .Permalink }}">{{ .Language.LanguageName }}</a>
{{- range .Translations }}
<a class="px-4 py-1 hover:text-eureka" href="{{ .Permalink }}">{{ .Language.LanguageName }}</a>
{{- end }}
</div>
</div>
{{- end }}
</div>
</div>
<div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-0" id="is-open-mobile">
</div>
</nav>
<script>
let element = document.getElementById('lightDarkMode')
{{- if eq .Site.Params.colorScheme "light" }}
if (storageColorScheme == 'Auto') {
element.firstElementChild.classList.remove('fa-sun')
element.firstElementChild.setAttribute("data-icon", 'adjust')
element.firstElementChild.classList.add('fa-adjust')
document.addEventListener('DOMContentLoaded', () => {
switchMode('Auto')
})
} else if (storageColorScheme == "Dark") {
element.firstElementChild.classList.remove('fa-sun')
element.firstElementChild.setAttribute("data-icon", 'moon')
element.firstElementChild.classList.add('fa-moon')
}
{{- else if eq .Site.Params.colorScheme "dark" }}
if (storageColorScheme == 'Auto') {
element.firstElementChild.classList.remove('fa-moon')
element.firstElementChild.setAttribute("data-icon", 'adjust')
element.firstElementChild.classList.add('fa-adjust')
document.addEventListener('DOMContentLoaded', () => {
switchMode('Auto')
})
} else if (storageColorScheme == "Light") {
element.firstElementChild.classList.remove('fa-moon')
element.firstElementChild.setAttribute("data-icon", 'sun')
element.firstElementChild.classList.add('fa-sun')
}
{{- else }}
if (storageColorScheme == null || storageColorScheme == 'Auto') {
document.addEventListener('DOMContentLoaded', () => {
switchMode('Auto')
})
} else if (storageColorScheme == "Light") {
element.firstElementChild.classList.remove('fa-adjust')
element.firstElementChild.setAttribute("data-icon", 'sun')
element.firstElementChild.classList.add('fa-sun')
} else if (storageColorScheme == "Dark") {
element.firstElementChild.classList.remove('fa-adjust')
element.firstElementChild.setAttribute("data-icon", 'moon')
element.firstElementChild.classList.add('fa-moon')
}
{{- end }}
document.addEventListener('DOMContentLoaded', () => {
getcolorscheme();
switchBurger();
{{- if .IsTranslated }}
switchLanguage()
{{- end }}
});
</script>
大部分的内容都是 Eureka 主题提供的,除了增加了 #search-container
搜索框部分。为了让搜索框更美观一点,我在 layouts/partials/custom_head.html
中自定义了一些样式。
.search-container {
margin-top: -0.3rem;
}
.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;
}
最终搜索框跳转至单独的搜索页面。关于如何给 Hugo 博客添加搜索功能,可查看我之前的文章 《Hugo 之旅》[7]。我这边提供一下我的搜索结果页模板。
<!--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="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.0/fuse.min.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.0,
tokenize: true,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: "title",
weight: 0.8
},
{
name: "summary",
weight: 0.5
},
{
name: "tags",
weight: 0.3
},
{
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">
<i class="fas fa-calendar mr-1"></i>
<span>${item.date}</span>
</div>
<div class="mr-6 my-2">
<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">
<i class="fas fa-clock mr-1"></i>
<span>${item.time} 分钟阅读时长</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 }}
之前使用 Typecho 的时候有一个归档插件会按照年月列表展示文章,所以我在 Hugo 中按照之前的格式实现了一下。按照如下内容新建 layouts/_default/archive.html
文件,并新建文章 content/日志.md
,文章内容为空即可,在文章的 meta 数据中指定 layout: archive
来映射到该模板。
<!--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>
<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 "get_featured" . }}
{{ with $featured }}
<div class="my-4">
{{ . }}
</div>
{{ end }}
<div class="content">
<script type='text/javascript' src="https://lib.baomitu.com/jquery/1.11.1/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">展开全部</a>
<ul class="car-list">
{{ range (.Site.RegularPages.GroupByDate "01月 2006") }}
<li>
<span class="car-yearmonth">{{ .Key }} <span title="Post Count">({{ len .Pages }})</span></span>
<ul class="car-monthlisting">
{{ range .Pages }}
<li>
{{ .Date.Format "02"}}: <a href="{{ .Permalink }}">{{ .Title }} </a> <!--<span title="Comment Count">(0)</span>-->
</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
</div>
</div>
</div>
{{ end }}
按照如下内容新建 layouts/_default/stats.html
文件,并新建文章 content/统计.md
,文章内容为空即可,在文章的 meta 数据中指定 layout: stats
来映射到该模板。
<!--layouts/_default/stats.html-->
{{ define "main" }}
{{- $.Scratch.Add "stats" slice -}}
{{- range .Site.RegularPages -}}
{{$resp := getJSON "https://imnerd.vercel.app/comment?type=count&url=/" .Slug ".html" }}
{{- $.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") "comments" $resp "count" .WordCount) -}}
{{- end -}}
{{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }}
{{ $hasSidebar := or ($hasToc) (.Params.series) }}
<style>
.chart {
margin-top: 15px;
width: 100%;
height: 350px;
}
</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>
<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 "get_featured" . }}
{{ with $featured }}
<div class="my-4">
{{ . }}
</div>
{{ end }}
<div class="content">
{{ .Content }}
</div>
</div>
{{ if $hasSidebar}}
<div class="col-span-2">
{{ if .GetTerms "series" }}
{{ partial "post_series.html" . }}
{{ end }}
{{ if $hasToc }}
{{ partial "post_toc.html" . }}
{{ end }}
</div>
{{ end }}
</div>
<script src="https://lib.baomitu.com/echarts/5.0.0/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 = parseInt(document.querySelector('footer span').innerText.match(/\d+/)[0]);
const pages = data.length - articles;
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="/">${articles}</a></strong> 篇</span>;<span>页面:<strong><a href="/">${pages}</a></strong> 篇</span>;<span>评论:<strong>${comments}</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 = 2009; 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 = 2009; 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 }}
除了以上这些,我的博客中改动最大的当属评论这块,但这块定制性比较高,一般玩家就不推荐了,感兴趣的还是去看我之前的《静态博客如何高性能插入评论》[9]一文。除此之外,我还修改了 footer.html
修改了底部显示文案,增加了网页统计脚本;基于自研的 wxhermit[10] 增加了微信分享自定义相关功能;文章页目录底部增加了个人公众号的展示。由于这些内容都比较简单且定制化内容程度比较高,就不一一展示了,感兴趣的朋友可以自行查看源码查阅。
[1]Eureka: https://github.com/wangchucheng/hugo-eureka
[2]Eureka: https://github.com/wangchucheng/hugo-eureka
[3]《静态博客如何高性能插入评论》: https://imnerd.org/hugo-ssr-comment.html
[4]最近评论挂件: https://waline.js.org/client/recentcomment.html
[5]forestry: https://forestry.io/
[6]《Hugo 之旅》: https://imnerd.org/hugo-start.html#%E5%85%B6%E5%AE%83
[7]《Hugo 之旅》: https://imnerd.org/hugo-start.html#%E6%90%9C%E7%B4%A2
[8]统计页面: https://imququ.com/post/blog-stats.html
[9]《静态博客如何高性能插入评论》: https://imnerd.org/hugo-ssr-comment.html
[10]wxhermit: https://github.com/lizheming/wxhermit
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8