今天有网友邮件我咨询我现在的主题 Eureka 的一些自定义配置,他想参考一下。由于我的博客仓库是私有的,所以就写一篇文章简单整理一下。
Eureka 是前段时间群友推荐给我的,纯白的朴素风格同时提供了暗色模式瞬间我就喜欢上了。将其 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"> {{ $walineURL := .Site.Params.comment.waline.serverURL }} <h2 class="widget-title ">最近回复</h2> <ul class="widget-list recentcomments"> {{ $resp := getJSON $walineURL "/comment?type=recent&count=10" }} {{ range first 10 $resp }} <li class="recentcomments"> <a href="{{.Site.BaseURL}}{{ .url }}">{{ .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 "components/summary-plain.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 评论系统并进行了一定的改造,具体可参见我之前的文章《静态博客如何高性能插入评论》。当然也可以直接使用 Waline 自带的最近评论挂件。
友情链接则是在 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 提供的服务,它支持提供在线后台进行文章、页面和其它配置的管理。评论管理则是链接到了 Waline 服务的后台面板中。
Metadata Eureka 主题的文章 metadata 显示分为列表页和详情页两个,分别对应 post_metadata.html 和 post_metadata_full.html 两个文件。我们在 layouts/partials/ 目录下新建这两个文件用来覆盖主题默认的文件。
2021-03-13 更新: 更新后的 Eureka 统一使用了 components/post-metadata.html 显示文章的 metadata,代码和之前的 layouts/partials/post_metadata.html 是一致的。
{{/* layouts/partials/components/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 之旅》。
{{/* 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://imerd.comment.lithub.cc/comment?type=count&url=https://imnerd.org/" .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 px-4 py-4 md:py-0"> <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="md:flex md:h-16 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 md:h-(16-4px) md:leading-(16-4px) box-border md:border-t-2 md:border-b-2 {{ if hasPrefix $relPermalink $url }} selected-menu-item {{ else }} border-transparent {{ end }} 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" name="Light">{{i18n "light"}}</span> <span class="px-4 py-1 hover:text-eureka" name="Dark">{{i18n "dark"}}</span> <span class="px-4 py-1 hover:text-eureka" name="Auto">{{i18n "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 之旅》。我这边提供一下我的搜索结果页模板。
{{/* 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 "utils/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 -}} {{- $.Scratch.Add "stats" (dict "title" .Title "slug" .Slug "year" (.Date.Format "2006") "month" (.Date.Format "2006-01") "hour" (.Date.Format "15") "week" (.Date.Format "Monday") "count" .WordCount) -}} {{- end -}} {{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }} {{ $hasSidebar := or ($hasToc) (.Params.series) }} <style> .chart { margin-top: 15px; width: 100%; height: 350px; } </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 "utils/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 "components/post-series.html" . }} {{ end }} {{ if $hasToc }} {{ partial "components/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 = {{ len (where .Site.RegularPages "Section" "posts") }}; 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>${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 }} 其它 除了以上这些,我的博客中改动最大的当属评论这块,但这块定制型比较高,一般玩家就不推荐了,感兴趣的还是去看我之前的《静态博客如何高性能插入评论》一文。除此之外,我还修改了 footer.html 修改了底部显示文案,增加了网页统计脚本;基于自研的 wxhermit 增加了微信分享自定义相关功能;文章页目录底部增加了个人公众号的展示。由于这些内容都比较简单且定制化内容程度比较高,就不一一展示了,感兴趣的朋友可以自行查看源码查阅。