Hugo 主题 Eureka 自定义

408次阅读  |  发布于3年以前

今天有网友邮件咨询我现在的主题 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 服务的后台面板中。

Metadata

Eureka 主题的文章 metadata 显示分为列表页和详情页两个,分别对应 post_metadata.htmlpost_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