本文最后更新于 114 天前 ,文中信息可能已经过时。如有问题请在评论区留言。

前言

之前使用 Halo 时,可以通过插件实现瞬间页面,类似发朋友圈一样。对我来说,一些简单的内容又不想单独作为一篇文章,瞬间页可以很好的满足需求。 但是转用 hugo 后,搜索了很多教程和优秀的博主站点都没有该功能。抄不了那没办法只能硬着头皮自己码代码了。

如果你不想了解实现过程,可直接跳转到 完整代码,直接复制完整代码。 如果你使用的是本站开源主题 PaperMod-PE 可直接跳转 使用教程

效果展示

你可以前往 🌟 瞬间 预览本站最新的实现效果。

image-20240524085647185

基本思路

目标:使用一个文件夹例如 moments 来作为瞬间页内容的来源,每条瞬间一个 md 文件。

页面模板

为了展示瞬间的内容,第一步新建一个瞬间页面模板,来容纳每一条瞬间。

基于我们的目标,需要将多个 md 文件内容容纳到一个页面,并且考虑到未来内容很多时的还需要分页展示,所以这里采用 Section page templates

根据文档 Section pages 可知,我们可以使用多种形式来创建这个界面模板。我们这里以 layouts/section/list.html 形式为例。 创建 list.html,路径为 layouts/moments/list.html,瞬间界面结构在此定义。

本站瞬间界面结构参考微信朋友圈实现。

Build options

基于我们的目标,将多个 md 文件整合到一个界面后,那么就不应该在单独渲染每一条瞬间原本的界面,否则会污染整个站点的结构(如 site.Pages 包含很多瞬间的内容,这不是我们希望看到的)。 通过查阅官方文档,了解到 Hugo Build options。这也是我们实现这个功能的一个核心。

根据文档内容,我们可以创建 _index.md:

markdown
content/moments/_index.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
title: "🌟 瞬间"
build:
  render: always
cascade:
  - build:
      list: local
      publishResources: false
      render: never
---

这样我们重启 hugo,就可以访问 http://localhost:1313/moments/ 界面了,并且 content/moments 目录下所有 md 都不会被渲染成页面。

评论

本文采用 Giscus。如果你也希望集成 Giscus,可参考本站教程:PaperMod 集成 Giscus 评论

根据 Giscus 官方文档 可知,我们可以通过修改参数 data-mapping="specific" 并指定 data-term, 来自定义页面 ↔️ discussion 映射关系。 这使得我们能为每个瞬间实现评论功能。

由于 giscus 实现方式,没办法同时显示所有瞬间评论内容,因此采用更简单粗暴的方式,用户主动点击右下角评论按钮再展示评论区。 通过在页面模板中,将每个瞬间的 slug 赋值到评论按钮的属性 (data-slug) 上,然后通过这个值在 JS 中动态生成每个瞬间的 data-term

如果你的站点使用的不是 Giscus,你可以参考你的评论系统的官方文档来修改评论按钮的点击方法 function showComment(element)

使用教程

PaperMod-PE

请先更新到最新版本。

创建 _index.md: (内容参考如下)

markdown
content/moments/_index.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
---
title: "🌟 瞬间"
DateFormat: 2006-01-02 15:04
build:
  render: always
cascade:
  - build:
      list: local
      publishResources: false
      render: never
---

DateFormat:修改瞬间显示的时间格式。你可以参考 官方文档 自定义你的时间格式。 如果未配置此项,则默认使用站点的 DateFormat。即:(以下为示例)

yaml
hugo.yml
1
2
params:
  DateFormat: 2006-01-02

这样你就可以访问瞬间界面 /moments

如何新增一条瞬间

moments 目录下创建一个 md 文档,frontmatter 参考如下:

markdown
1
2
3
4
5
6
7
8
---
date: 2024-03-13T09:05:00+08:00
slug: "change to your moment slug"
tags:
  - Apple
draft: false
---
enter your moment content.

参数说明:

  • date 是可选的。在瞬间左下角显示的时间。(建议显示指定该值,如果你未配置此项,也可能显示时间,因为赋值方式为 .Param "date"
  • slug 是必须的。它涉及到与评论绑定,建议使用 UUID 或随机数来保证不重复。
  • tags 是可选的。标记这条瞬间的标签,可以为多个。(注意,此标签与文章标签无关)
  • hideComment 是可选的。如果为 true 则不会在这个瞬间的右下角显示评论按钮。

完整代码

最新源码请参考 PaperMod-PE

moments.html

html
layouts/moments/list.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
{{- define "main" }}
{{- $paginator := .Paginate .Pages }}
{{ $dateformat := .Params.DateFormat }}

<article class="post-single">
    <header class="page-header">
        <h1>
            {{- (printf "%s&nbsp;" .Title ) | htmlUnescape -}}
        </h1>
        {{- if .Description }}
        <div class="post-description">
            {{ .Description }}
        </div>
        {{- end }}
    </header>

    <div class="post-content">
        <div class="pe-moments">
            <ul>
                {{- range $moment := $paginator.Pages }}
                {{- if .Content }}
                <li class="pe-moment">
                    <img src="{{ site.Params.label.icon }}" alt="{{ site.Params.author }}">
                    <div class="pe-moment-body">
                        <div class="pe-moment-content">
                            {{ .Content }}
                        </div>
                        {{ if .Params.tags }}
                        <div class="pe-moment-tags">
                            {{- range $index, $tag := (.Params.tags) }}
                            <span class="pe-moment-tag">{{ $tag }}</span>
                            {{- end }}
                        </div>
                        {{ end }}
                        <div class="pe-moment-bottom">
                            <div class="pe-moment-time">
                                <span>{{ $moment.Param "date" | time.Format (default site.Params.DateFormat $dateformat) }}</span>
                            </div>
                            {{ if not .Params.hideComment }}
                            <button class="pe-moment-comment-btn" onclick="showComment(this)" data-slug="{{ $moment.Param "slug" }}">
                                <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M281.535354 387.361616c-31.806061 0-57.664646 26.763636-57.664647 59.733333 0 32.969697 25.858586 59.733333 57.664647 59.733334s57.664646-26.763636 57.664646-59.733334c0-33.09899-25.858586-59.733333-57.664646-59.733333z m230.529292 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.729293 59.733333 57.664646 59.733334 31.806061 0 57.535354-26.763636 57.535354-59.733334 0-33.09899-25.858586-59.733333-57.535354-59.733333z m230.4 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.858586 59.733333 57.664646 59.733334s57.664646-26.763636 57.664647-59.733334c-0.129293-33.09899-25.858586-59.733333-57.664647-59.733333z m115.2-270.222222H166.335354c-63.612121 0-115.2 53.527273-115.2 119.59596v390.981818c0 65.939394 52.751515 126.836364 117.785858 126.836363h175.579798c30.513131 32.581818 157.220202 149.979798 157.220202 149.979798 5.559596 5.818182 14.739394 5.818182 20.29899 0 0 0 92.832323-91.410101 153.212121-149.979798h179.717172c65.034343 0 117.785859-60.89697 117.785859-126.836363V236.606061c0.129293-65.939394-51.458586-119.466667-115.070708-119.466667z m57.535354 510.577778c0 32.969697-27.668687 67.620202-60.250505 67.620202H678.335354c-21.462626 0-40.727273 21.979798-40.727273 21.979798l-124.121212 114.941414-124.121212-114.941414s-23.660606-21.979798-43.830303-21.979798H168.921212c-32.581818 0-60.250505-34.650505-60.250505-67.620202V236.606061c0-32.969697 25.729293-59.733333 57.664647-59.733334h691.329292c31.806061 0 57.535354 26.763636 57.535354 59.733334v391.111111z m0 0"></path></svg>
                            </button>
                            {{ end }}
                        </div>
                    </div>
                </li>
                {{ end }}
                {{ end }}
            </ul>
        </div>
    </div>

</article>

{{- if gt $paginator.TotalPages 1 }}
<footer class="page-footer">
    <nav class="pagination">
        {{- if $paginator.HasPrev }}
        <a class="prev" href="{{ $paginator.Prev.URL | absURL }}">
            {{ i18n "prev_page" }}
            {{- if (.Param "ShowPageNums") }}
            {{- sub $paginator.PageNumber 1 }}/{{ $paginator.TotalPages }}
            {{- end }}
        </a>
        {{- end }}
        {{- if $paginator.HasNext }}
        <a class="next" href="{{ $paginator.Next.URL | absURL }}">
            {{- i18n "next_page" }}
            {{- if (.Param "ShowPageNums") }}
            {{- add 1 $paginator.PageNumber }}/{{ $paginator.TotalPages }}
            {{- end }}
        </a>
        {{- end }}
    </nav>
</footer>
{{- end }}

<script>
    function showComment(element) {
        const slug = element.getAttribute('data-slug');
        const commentElement = document.getElementById(slug);
        if (commentElement) {
            commentElement.remove();
            return;
        }
        const comments = document.getElementsByClassName("pe-moment-comment");
        if (comments) {
            for (let comment of comments) {
                comment.remove();
            }
        }
        const momentBody = element.closest('.pe-moment-body');
        let giscusAttributes = {
            "src": "https://giscus.app/client.js",
            "data-repo": "{{ .Site.Params.giscus.repo }}",
            "data-repo-id": "{{ .Site.Params.giscus.repoId }}",
            "data-category": "{{ .Site.Params.giscus.category }}",
            "data-category-id": "{{ .Site.Params.giscus.categoryId }}",
            "data-mapping": "{{ .Site.Params.giscus.mapping | default "pathname" }}",
            "data-term": "moments/" + slug,
            "data-strict": "{{ .Site.Params.giscus.strict | default "0" }}",
            "data-reactions-enabled": "{{ .Site.Params.giscus.reactionsEnabled | default "1" }}",
            "data-emit-metadata": "{{ .Site.Params.giscus.emitMetadata | default "0" }}",
            "data-input-position": "{{ .Site.Params.giscus.inputPosition | default "bottom" }}",
            "data-theme": getStoredTheme(),
            "data-lang": "{{ .Site.Params.giscus.lang | default "en" }}",
            "crossorigin": "anonymous",
            "async": "",
        };
        const commentDiv = document.createElement('div');
        commentDiv.id = slug;
        commentDiv.className = "pe-moment-comment";
        // 动态创建 giscus script
        let giscusScript = document.createElement("script");
        Object.entries(giscusAttributes).forEach(
                ([key, value]) => giscusScript.setAttribute(key, value));
        commentDiv.appendChild(giscusScript);
        momentBody.appendChild(commentDiv);
    }
    const getStoredTheme = () => localStorage.getItem("pref-theme") === "dark" ? "{{ .Site.Params.giscus.darkTheme }}" : "{{ .Site.Params.giscus.lightTheme }}";
    const setGiscusTheme = () => {
        const sendMessage = (message) => {
            const iframe = document.querySelector('iframe.giscus-frame');
            if (iframe) {
                iframe.contentWindow.postMessage({giscus: message}, 'https://giscus.app');
            }
        }
        sendMessage({setConfig: {theme: getStoredTheme()}})
    }
    document.addEventListener("DOMContentLoaded", () => {
        // 页面主题变更后,变更 giscus 主题
        const themeSwitcher = document.querySelector("#theme-toggle");
        if (themeSwitcher) {
            themeSwitcher.addEventListener("click", setGiscusTheme);
        }
        // 本站悬浮按钮,如果你没有则删除以下内容
        const themeFloatSwitcher = document.querySelector("#theme-toggle-float");
        if (themeFloatSwitcher) {
            themeFloatSwitcher.addEventListener("click", setGiscusTheme);
        }
    });
</script>
{{- end }}{{/* end main */}}

moments.css

css
assets/css/extended/moments.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
.list {
    background: #ffffff;
}

.page-header {
    margin: 0 auto 1rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

.pe-moment {
    padding: 2rem 0;
    gap: .8rem;
    align-items: flex-start;
    display: flex;
    border-bottom: 1px;
}

.pe-moments li {
    border-bottom: 1px solid #d6d6d6;
}

.pe-moments img {
    width: 4.8rem;
    height: 4.8rem;
    border-radius: 50%;
    margin: 0;
}

.pe-moments ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pe-moment-body {
    margin-left: 2.4rem;
    width: 100%;
    overflow: hidden;
}

.pe-moment-content {
    margin-bottom: 1.6rem;
}

.pe-moment-tag {
    display: inline-block;
    padding: 0.25em 0.6em;
    font-size: 0.875em;
    line-height: 1;
    color: #999999; /* 暗色字体 */
    background-color: #f0f0f0; /* 浅色背景 */
    border-radius: 0.5rem;
}

.dark .pe-moment-tag {
    background-color: #333; /* 暗色背景 */
}

.pe-moment-bottom {
    margin-top: 1.2rem;
    display: flex;
    align-items: center;
}

.pe-moment-time {
    display: inline-block;
    color: #999999;
}

.pe-moment-comment-btn svg {
    width: 2rem;
    height: 2rem;
    display: inline-block;
    vertical-align: 0.15em;
    fill: #fff;
}

.pe-moment-comment-btn {
    margin-left: auto;
    display: flex;
    align-items: center;
    background: rgb(214, 214, 214);
    border-radius: .5rem;
    padding: .2rem 1rem;
}

.dark .pe-moment-comment-btn {
    background: rgb(65, 66, 68);
}

.pe-moment-comment-btn:hover {
    background-color: #e26c56;
    border-radius: .5rem;
}

.pe-moment-comment {
    margin-top: .2rem;
}

.page-footer {
    margin-top: 2rem;
}