Hugo 主题 Eureka 自定义

发表于 3年以前  | 总阅读数:407 次

今天有网友邮件咨询我现在的主题 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

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/4_mQaJisIjPbbxzJ14e4ow

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录