跳过正文
  1. 文章/

Blowfish 主题魔改

·9361 字·19 分钟
SuburbiaXX
作者
SuburbiaXX
Life is full of regrets.
目录

前言
#

  • 感觉 Hexo 编译太慢了,但是又不想用 WordPress 这种,还是想找个静态博客的框架,于是就选择了 Hugo,找了个看起来还可以的主题 Blowfish

参考魔改
#

主题色
#

  • 参考上述文章进行了稍微调整,直接在 assets/css/custom.css 中添加如下代码覆盖默认样式
     1:root {
     2  --color-neutral: 255,255,255;
     3  --color-neutral-50:     250,250,249;
     4  --color-neutral-100:    245,245,244;
     5  --color-neutral-200:    231,229,228;
     6  --color-neutral-300:    214,211,209;
     7  --color-neutral-400:    168,162,158;
     8  --color-neutral-500:    120,113,108;
     9  --color-neutral-600:    87,83,78;
    10  --color-neutral-700:    68,64,60;
    11  --color-neutral-800:    41,37,36;
    12  --color-neutral-900:    28,25,23;
    13
    14  --color-primary-50:     255,255,255;
    15  --color-primary-100:    255,255,255;
    16  --color-primary-200:    255,255,255;
    17  --color-primary-300:    242,211,223;
    18  --color-primary-400:    225,151,181;
    19  --color-primary-500:    208,92,138;
    20  --color-primary-600:    199,60,115;
    21  --color-primary-700:    170,49,97;
    22  --color-primary-800:    138,40,79;
    23  --color-primary-900:    106,31,61;
    24
    25  --color-secondary-50:     255,255,255;
    26  --color-secondary-100:    255,255,255;
    27  --color-secondary-200:    255,255,255;
    28  --color-secondary-300:    255,242,219;
    29  --color-secondary-400:    255,215,143;
    30  --color-secondary-500:    255,188,66;
    31  --color-secondary-600:    255,174,25;
    32  --color-secondary-700:    239,155,0;
    33  --color-secondary-800:    199,128,0;
    34  --color-secondary-900:    158,102,0;
    35}

标题鼠标悬停
#

  • 直接参考上述文章的做法,在 assets/css/custom.css 中添加如下代码
    1/* 标题悬停时的颜色变化 */
    2nav a[href="/"]:hover {
    3  color: rgb(var(--color-primary-600));
    4}
    5/* 夜间模式下标题悬停时的颜色 */
    6.dark nav a[href="/"]:hover {
    7  color: rgb(var(--color-primary-400));
    8}

文章目录添加悬停样式
#

  • assets/css/custom.css 中添加如下代码
     1#TableOfContents a {
     2  position: relative;
     3  padding-left: 0.75em;
     4  display: block;
     5  transition: color 0.3s ease;
     6}
     7
     8#TableOfContents a::before {
     9  content: '';
    10  position: absolute;
    11  left: 0;
    12  top: 0.2em;
    13  bottom: 0.2em;
    14  width: 4px;
    15  background-color: rgb(var(--color-primary-500));
    16  transform: scaleY(0);
    17  transition: transform 0.3s ease;
    18  border-radius: 2px;
    19}
    20
    21#TableOfContents a:hover::before {
    22  transform: scaleY(1);
    23}

顶部阅读进度条
#

  • css 样式代码

     1/* 顶部阅读进度条 */
     2.top-scroll-bar {
     3    position: fixed;
     4    top: 0;
     5    left: 0;
     6    z-index: 9999;
     7    display: none;
     8    width: 0;
     9    height: 3px;
    10    background: rgb(var(--color-primary-400));
    11  }
  • 真正发挥作用的代码,新建一个 layouts/partials/extend-footer.html

     1<!-- 进度条逻辑 -->
     2<script>
     3    window.addEventListener('scroll', () => {
     4      const bar = document.querySelector('.top-scroll-bar');
     5      if (!bar) return;
     6      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
     7      const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
     8      const width = (scrollTop / scrollHeight) * 100;
     9      bar.style.width = width + '%';
    10      bar.style.display = 'block';
    11    });
    12</script>
  • 基于 Blowfish 主题提供的拓展选项,新建一个 layouts/partials/extend-head.html

    1<!-- 进度条 -->
    2<div class="top-scroll-bar"></div>

文章密码功能
#

  • themes/blowfish/layouts/_default/single.html 复制到 layouts/_default/single.html,然后在对应位置添加如下代码

     1{{ define "main" }}
     2  {{ .Scratch.Set "scope" "single" }}
     3  <article>
     4    {{/* Hero */}}
     5    {{ if .Params.showHero | default (site.Params.article.showHero | default false) }}
     6      {{ $heroStyle := .Params.heroStyle }}
     7      {{ if not $heroStyle }}{{ $heroStyle = site.Params.article.heroStyle }}{{ end }}
     8      {{ $heroStyle := print "hero/" $heroStyle ".html" }}
     9      {{ if templates.Exists ( printf "partials/%s" $heroStyle ) }}
    10        {{ partial $heroStyle . }}
    11      {{ else }}
    12        {{ partial "hero/basic.html" . }}
    13      {{ end }}
    14    {{ end }}
    15    <!-- 文章密码功能 -->
    16    {{ if ( .Params.password | default "" ) }}
    17    <script>
    18        (function(){
    19            if (prompt('请输入文章密码') != "{{ .Params.password }}"){
    20                alert('密码错误!');
    21                if (history.length === 1) {
    22                    window.opener = null;
    23                    window.open('', '_self');
    24                    window.close();
    25                } else {
    26                    history.back();
    27                }
    28            }
    29        })();
    30    </script>
    31    {{ end }}
    32    <!-- 文章密码功能结束 -->
  • 需要加密码的文章只需要在 Front Matter 中添加 password: xxx 字段即可

短代码:防剧透的文本高斯模糊样式
#

  • layouts/shortcodes 新建 blur.html

     1<span class="blur">{{.Inner | markdownify}}</span>
     2
     3<style>
     4    /* 文本高斯模糊 */
     5    .blur {
     6        filter: blur(4px);
     7        transition: filter 0.3s ease;
     8    }
     9    .blur:hover {
    10        filter: blur(0);
    11    }
    12</style>
  • 使用方法

    1{{< blur >}}想要高斯模糊的文本{{</ blur >}}
  • 不受任何主题限制,鼠标悬停或屏幕点击后文字正常显示,且内文本支持 markdown 格式。

友链卡片
#

  • Hugo | NeoDB 短代码及 Blowfish 友链样式

  • 修改了一下卡片背景和 hover 效果,支持分组和对应描述字段(组内乱序)

  • 新建一个 layouts/_default/links.html,其中头像获取失败默认图片的 placeholder 为 img/default.jpg,记得替换

      1{{ define "main" }}
      2{{ .Scratch.Set "scope" "single" }}
      3<article>
      4    {{ if .Params.showHero | default (.Site.Params.article.showHero | default false) }}
      5    {{ $heroStyle := .Params.heroStyle }}
      6    {{ if not $heroStyle }}{{ $heroStyle = .Site.Params.article.heroStyle }}{{ end }}
      7    {{ $heroStyle := print "hero/" $heroStyle ".html" }}
      8  {{ if templates.Exists ( printf "partials/%s" $heroStyle ) }}
      9    {{ partial $heroStyle . }}
     10    {{ else }}
     11    {{ partial "hero/basic.html" . }}
     12    {{ end }}
     13    {{ end }}
     14
     15    <header id="single_header" class="mt-5 max-w-prose">
     16        {{ if .Params.showBreadcrumbs | default (.Site.Params.article.showBreadcrumbs | default false) }}
     17        {{ partial "breadcrumbs.html" . }}
     18        {{ end }}
     19        <h1 class="mt-0 text-4xl font-extrabold text-neutral-900 dark:text-neutral">
     20            {{ .Title }}
     21        </h1>
     22
     23        <div class="mt-1 mb-6 text-base text-neutral-500 dark:text-neutral-400 print:hidden">
     24            {{ partial "article-meta/basic.html" (dict "context" . "scope" "single") }}
     25        </div>
     26
     27    </header>
     28
     29    <section class="flex flex-col max-w-full mt-0 prose dark:prose-invert dark:marker:text-neutral-400 lg:flex-row">
     30
     31            <div class="min-w-0 min-h-0 max-w-fit">
     32
     33                <div class="article-content max-w-full mb-20 break-words">
     34
     35                        {{ .Content }}
     36
     37                        <!-- 添加 not-prose 类来避免 prose 样式的影响 -->
     38                        <div class="not-prose">
     39                                {{ $dataFile := .Params.linksFile }}
     40                                {{ if not $dataFile }}
     41                                    {{ errorf "linksFile param is missing in front matter for page %s" .File.Path }}
     42                                {{ end }}
     43                                {{ $linksResource := resources.Get $dataFile }}
     44                                {{ if $linksResource }}
     45                                    {{ $data := $linksResource | transform.Unmarshal }}
     46
     47                                    {{/* Determine if we are dealing with a list of categories or a single object with a 'links' key */}}
     48                                    {{ $categories := slice }}
     49
     50                                    {{ if reflect.IsSlice $data }}
     51                                        {{ $categories = $data }}
     52                                    {{ else if reflect.IsMap $data }}
     53                                        {{ if isset $data "links" }}
     54                                            {{/* Convert old format to new format structure for consistent rendering */}}
     55                                            {{ $categories = slice (dict "link_list" $data.links) }}
     56                                        {{ end }}
     57                                    {{ end }}
     58
     59                                    {{ range $categories }}
     60                                        {{ if .class_name }}
     61                                            <h2 class="mt-8 mb-4 text-2xl font-bold text-neutral-900 dark:text-neutral-100 flex items-center">
     62                                                {{ if .class_icon }}<i class="{{ .class_icon }} mr-2"></i>{{ end }}
     63                                                {{ .class_name }}
     64                                            </h2>
     65                                        {{ end }}
     66                                        {{ if .class_desc }}
     67                                            <p class="mb-4 text-neutral-600 dark:text-neutral-400">{{ .class_desc }}</p>
     68                                        {{ else }}
     69                                            <p class="mb-4 text-neutral-600 dark:text-neutral-400">{{ "&nbsp;" | safeHTML }}</p>
     70                                        {{ end }}
     71
     72                                        <div class="friends-links grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
     73                                            {{ range .link_list }}
     74                                            {{ $name := .name }}
     75                                            {{ $bio := .bio | default .descr }}
     76                                            {{ $url := .url | default .link }}
     77                                            {{ $avatar := .avatar }}
     78
     79                                            <a target="_blank" href="{{ $url }}" title="{{ $name }}" class="block h-full" rel="noopener noreferrer">
     80                                                <div class="friend-card group flex items-center p-4 bg-white dark:bg-neutral-800 rounded-xl shadow-sm border border-neutral-200 dark:border-neutral-700 h-full">
     81                                                <div class="flex-shrink-0 mr-4">
     82                                                    <div class="friend-avatar-wrapper">
     83                                                    <img class="friend-avatar lazy nozoom w-16 h-16 object-cover rounded-full block" loading="lazy"
     84                                                        src="{{ $avatar }}"
     85                                                        alt="{{ $name }}"
     86                                                        onerror="this.onerror=null; this.src='img/default.jpg';"
     87                                                        style="width: 64px; height: 64px;">
     88                                                    </div>
     89                                                </div>
     90                                                <span class="inline-block w-4 h-4 mr-3"></span>
     91                                                <div class="flex-grow min-w-0">
     92                                                    <div class="flex items-center mb-2">
     93                                                    <span class="font-medium text-neutral-900 dark:text-neutral-100 truncate" style="strong">{{ $name }}</span>
     94                                                    </div>
     95                                                    <p class="text-sm text-neutral-700 dark:text-neutral-300 line-clamp-2 m-0">{{ $bio }}</p>
     96                                                </div>
     97                                                </div>
     98                                            </a>
     99                                            {{ end }}
    100                                        </div>
    101                                    {{ end }}
    102                                {{ else }}
    103                                    {{ errorf "Could not find data file: %s" $dataFile }}
    104                                {{ end }}
    105                        </div>
    106
    107                </div>
    108
    109            </div>
    110
    111        </section>
    112
    113</article>
    114
    115{{ $lazyloadJS := resources.Get "js/lazyload.iife.min.js" | fingerprint "sha256" }}
    116<script type="text/javascript" src="{{ $lazyloadJS.RelPermalink }}" integrity="{{ $lazyloadJS.Data.Integrity }}"></script>
    117
    118<script>
    119        // Random links
    120        function shuffleArray(array) {
    121                for (let i = array.length - 1; i > 0; i--) {
    122                        const j = Math.floor(Math.random() * (i + 1));
    123                        [array[i], array[j]] = [array[j], array[i]];
    124                }
    125                return array;
    126        }
    127
    128        function randomizeLinks() {
    129                const linkContainers = document.querySelectorAll('.friends-links');
    130
    131                linkContainers.forEach(container => {
    132                        const links = Array.from(container.querySelectorAll('a'));
    133                        const shuffledLinks = shuffleArray(links);
    134
    135                        links.forEach(link => link.remove());
    136
    137                        shuffledLinks.forEach(link => container.appendChild(link));
    138                });
    139
    140                // Equalize heights after shuffling
    141                setTimeout(equalizeHeights, 0);
    142        }
    143
    144        function equalizeHeights() {
    145                const cards = document.querySelectorAll('.friend-card');
    146                let maxHeight = 0;
    147
    148                // Reset height to auto to get natural height
    149                cards.forEach(card => {
    150                        card.style.height = 'auto';
    151                });
    152
    153                // Find max height
    154                cards.forEach(card => {
    155                        if (card.offsetHeight > maxHeight) {
    156                                maxHeight = card.offsetHeight;
    157                        }
    158                });
    159
    160                // Apply max height to all cards
    161                cards.forEach(card => {
    162                        card.style.height = maxHeight + 'px';
    163                });
    164        }
    165
    166        randomizeLinks();
    167
    168        // Re-calculate on window resize
    169        window.addEventListener('resize', equalizeHeights);
    170</script>
    171
    172<script>
    173        var lazyLoadInstance = new LazyLoad({
    174                // Your custom settings go here
    175        });
    176</script>
    177{{ end }}
  • 新建一个图片懒加载的 js 代码 assets/js/lazyload.iife.min.js

      1var LazyLoad = (function () {
      2  "use strict";
      3  const e = "undefined" != typeof window,
      4    t =
      5      (e && !("onscroll" in window)) ||
      6      ("undefined" != typeof navigator &&
      7        /(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent)),
      8    a = e && window.devicePixelRatio > 1,
      9    s = {
     10      elements_selector: ".lazy",
     11      container: t || e ? document : null,
     12      threshold: 300,
     13      thresholds: null,
     14      data_src: "src",
     15      data_srcset: "srcset",
     16      data_sizes: "sizes",
     17      data_bg: "bg",
     18      data_bg_hidpi: "bg-hidpi",
     19      data_bg_multi: "bg-multi",
     20      data_bg_multi_hidpi: "bg-multi-hidpi",
     21      data_bg_set: "bg-set",
     22      data_poster: "poster",
     23      class_applied: "applied",
     24      class_loading: "loading",
     25      class_loaded: "loaded",
     26      class_error: "error",
     27      class_entered: "entered",
     28      class_exited: "exited",
     29      unobserve_completed: !0,
     30      unobserve_entered: !1,
     31      cancel_on_exit: !0,
     32      callback_enter: null,
     33      callback_exit: null,
     34      callback_applied: null,
     35      callback_loading: null,
     36      callback_loaded: null,
     37      callback_error: null,
     38      callback_finish: null,
     39      callback_cancel: null,
     40      use_native: !1,
     41      restore_on_error: !1,
     42    },
     43    n = (e) => Object.assign({}, s, e),
     44    l = function (e, t) {
     45      let a;
     46      const s = "LazyLoad::Initialized",
     47        n = new e(t);
     48      try {
     49        a = new CustomEvent(s, { detail: { instance: n } });
     50      } catch (e) {
     51        (a = document.createEvent("CustomEvent")),
     52          a.initCustomEvent(s, !1, !1, { instance: n });
     53      }
     54      window.dispatchEvent(a);
     55    },
     56    o = "src",
     57    r = "srcset",
     58    i = "sizes",
     59    c = "poster",
     60    d = "llOriginalAttrs",
     61    _ = "data",
     62    u = "loading",
     63    g = "loaded",
     64    b = "applied",
     65    h = "error",
     66    m = "native",
     67    p = "data-",
     68    v = "ll-status",
     69    f = (e, t) => e.getAttribute(p + t),
     70    E = (e) => f(e, v),
     71    I = (e, t) =>
     72      ((e, t, a) => {
     73        const s = p + t;
     74        null !== a ? e.setAttribute(s, a) : e.removeAttribute(s);
     75      })(e, v, t),
     76    k = (e) => I(e, null),
     77    A = (e) => null === E(e),
     78    L = (e) => E(e) === m,
     79    y = [u, g, b, h],
     80    w = (e, t, a, s) => {
     81      e &&
     82        "function" == typeof e &&
     83        (void 0 === s ? (void 0 === a ? e(t) : e(t, a)) : e(t, a, s));
     84    },
     85    C = (t, a) => {
     86      e && "" !== a && t.classList.add(a);
     87    },
     88    O = (t, a) => {
     89      e && "" !== a && t.classList.remove(a);
     90    },
     91    x = (e) => e.llTempImage,
     92    M = (e, t) => {
     93      if (!t) return;
     94      const a = t._observer;
     95      a && a.unobserve(e);
     96    },
     97    z = (e, t) => {
     98      e && (e.loadingCount += t);
     99    },
    100    N = (e, t) => {
    101      e && (e.toLoadCount = t);
    102    },
    103    R = (e) => {
    104      let t = [];
    105      for (let a, s = 0; (a = e.children[s]); s += 1)
    106        "SOURCE" === a.tagName && t.push(a);
    107      return t;
    108    },
    109    T = (e, t) => {
    110      const a = e.parentNode;
    111      a && "PICTURE" === a.tagName && R(a).forEach(t);
    112    },
    113    G = (e, t) => {
    114      R(e).forEach(t);
    115    },
    116    D = [o],
    117    H = [o, c],
    118    V = [o, r, i],
    119    F = [_],
    120    B = (e) => !!e[d],
    121    J = (e) => e[d],
    122    S = (e) => delete e[d],
    123    j = (e, t) => {
    124      if (B(e)) return;
    125      const a = {};
    126      t.forEach((t) => {
    127        a[t] = e.getAttribute(t);
    128      }),
    129        (e[d] = a);
    130    },
    131    P = (e, t) => {
    132      if (!B(e)) return;
    133      const a = J(e);
    134      t.forEach((t) => {
    135        ((e, t, a) => {
    136          a ? e.setAttribute(t, a) : e.removeAttribute(t);
    137        })(e, t, a[t]);
    138      });
    139    },
    140    U = (e, t, a) => {
    141      C(e, t.class_applied),
    142        I(e, b),
    143        a && (t.unobserve_completed && M(e, t), w(t.callback_applied, e, a));
    144    },
    145    $ = (e, t, a) => {
    146      C(e, t.class_loading),
    147        I(e, u),
    148        a && (z(a, 1), w(t.callback_loading, e, a));
    149    },
    150    q = (e, t, a) => {
    151      a && e.setAttribute(t, a);
    152    },
    153    K = (e, t) => {
    154      q(e, i, f(e, t.data_sizes)),
    155        q(e, r, f(e, t.data_srcset)),
    156        q(e, o, f(e, t.data_src));
    157    },
    158    Q = {
    159      IMG: (e, t) => {
    160        T(e, (e) => {
    161          j(e, V), K(e, t);
    162        }),
    163          j(e, V),
    164          K(e, t);
    165      },
    166      IFRAME: (e, t) => {
    167        j(e, D), q(e, o, f(e, t.data_src));
    168      },
    169      VIDEO: (e, t) => {
    170        G(e, (e) => {
    171          j(e, D), q(e, o, f(e, t.data_src));
    172        }),
    173          j(e, H),
    174          q(e, c, f(e, t.data_poster)),
    175          q(e, o, f(e, t.data_src)),
    176          e.load();
    177      },
    178      OBJECT: (e, t) => {
    179        j(e, F), q(e, _, f(e, t.data_src));
    180      },
    181    },
    182    W = ["IMG", "IFRAME", "VIDEO", "OBJECT"],
    183    X = (e, t) => {
    184      !t ||
    185        ((e) => e.loadingCount > 0)(t) ||
    186        ((e) => e.toLoadCount > 0)(t) ||
    187        w(e.callback_finish, t);
    188    },
    189    Y = (e, t, a) => {
    190      e.addEventListener(t, a), (e.llEvLisnrs[t] = a);
    191    },
    192    Z = (e, t, a) => {
    193      e.removeEventListener(t, a);
    194    },
    195    ee = (e) => !!e.llEvLisnrs,
    196    te = (e) => {
    197      if (!ee(e)) return;
    198      const t = e.llEvLisnrs;
    199      for (let a in t) {
    200        const s = t[a];
    201        Z(e, a, s);
    202      }
    203      delete e.llEvLisnrs;
    204    },
    205    ae = (e, t, a) => {
    206      ((e) => {
    207        delete e.llTempImage;
    208      })(e),
    209        z(a, -1),
    210        ((e) => {
    211          e && (e.toLoadCount -= 1);
    212        })(a),
    213        O(e, t.class_loading),
    214        t.unobserve_completed && M(e, a);
    215    },
    216    se = (e, t, a) => {
    217      const s = x(e) || e;
    218      ee(s) ||
    219        ((e, t, a) => {
    220          ee(e) || (e.llEvLisnrs = {});
    221          const s = "VIDEO" === e.tagName ? "loadeddata" : "load";
    222          Y(e, s, t), Y(e, "error", a);
    223        })(
    224          s,
    225          (n) => {
    226            ((e, t, a, s) => {
    227              const n = L(t);
    228              ae(t, a, s),
    229                C(t, a.class_loaded),
    230                I(t, g),
    231                w(a.callback_loaded, t, s),
    232                n || X(a, s);
    233            })(0, e, t, a),
    234              te(s);
    235          },
    236          (n) => {
    237            ((e, t, a, s) => {
    238              const n = L(t);
    239              ae(t, a, s),
    240                C(t, a.class_error),
    241                I(t, h),
    242                w(a.callback_error, t, s),
    243                a.restore_on_error && P(t, V),
    244                n || X(a, s);
    245            })(0, e, t, a),
    246              te(s);
    247          }
    248        );
    249    },
    250    ne = (e, t, s) => {
    251      ((e) => W.indexOf(e.tagName) > -1)(e)
    252        ? ((e, t, a) => {
    253            se(e, t, a),
    254              ((e, t, a) => {
    255                const s = Q[e.tagName];
    256                s && (s(e, t), $(e, t, a));
    257              })(e, t, a);
    258          })(e, t, s)
    259        : ((e, t, s) => {
    260            ((e) => {
    261              e.llTempImage = document.createElement("IMG");
    262            })(e),
    263              se(e, t, s),
    264              ((e) => {
    265                B(e) || (e[d] = { backgroundImage: e.style.backgroundImage });
    266              })(e),
    267              ((e, t, s) => {
    268                const n = f(e, t.data_bg),
    269                  l = f(e, t.data_bg_hidpi),
    270                  r = a && l ? l : n;
    271                r &&
    272                  ((e.style.backgroundImage = `url("${r}")`),
    273                  x(e).setAttribute(o, r),
    274                  $(e, t, s));
    275              })(e, t, s),
    276              ((e, t, s) => {
    277                const n = f(e, t.data_bg_multi),
    278                  l = f(e, t.data_bg_multi_hidpi),
    279                  o = a && l ? l : n;
    280                o && ((e.style.backgroundImage = o), U(e, t, s));
    281              })(e, t, s),
    282              ((e, t, a) => {
    283                const s = f(e, t.data_bg_set);
    284                if (!s) return;
    285                let n = s.split("|").map((e) => `image-set(${e})`);
    286                (e.style.backgroundImage = n.join()), U(e, t, a);
    287              })(e, t, s);
    288          })(e, t, s);
    289    },
    290    le = (e) => {
    291      e.removeAttribute(o), e.removeAttribute(r), e.removeAttribute(i);
    292    },
    293    oe = (e) => {
    294      T(e, (e) => {
    295        P(e, V);
    296      }),
    297        P(e, V);
    298    },
    299    re = {
    300      IMG: oe,
    301      IFRAME: (e) => {
    302        P(e, D);
    303      },
    304      VIDEO: (e) => {
    305        G(e, (e) => {
    306          P(e, D);
    307        }),
    308          P(e, H),
    309          e.load();
    310      },
    311      OBJECT: (e) => {
    312        P(e, F);
    313      },
    314    },
    315    ie = (e, t) => {
    316      ((e) => {
    317        const t = re[e.tagName];
    318        t
    319          ? t(e)
    320          : ((e) => {
    321              if (!B(e)) return;
    322              const t = J(e);
    323              e.style.backgroundImage = t.backgroundImage;
    324            })(e);
    325      })(e),
    326        ((e, t) => {
    327          A(e) ||
    328            L(e) ||
    329            (O(e, t.class_entered),
    330            O(e, t.class_exited),
    331            O(e, t.class_applied),
    332            O(e, t.class_loading),
    333            O(e, t.class_loaded),
    334            O(e, t.class_error));
    335        })(e, t),
    336        k(e),
    337        S(e);
    338    },
    339    ce = ["IMG", "IFRAME", "VIDEO"],
    340    de = (e) => e.use_native && "loading" in HTMLImageElement.prototype,
    341    _e = (e, t, a) => {
    342      e.forEach((e) =>
    343        ((e) => e.isIntersecting || e.intersectionRatio > 0)(e)
    344          ? ((e, t, a, s) => {
    345              const n = ((e) => y.indexOf(E(e)) >= 0)(e);
    346              I(e, "entered"),
    347                C(e, a.class_entered),
    348                O(e, a.class_exited),
    349                ((e, t, a) => {
    350                  t.unobserve_entered && M(e, a);
    351                })(e, a, s),
    352                w(a.callback_enter, e, t, s),
    353                n || ne(e, a, s);
    354            })(e.target, e, t, a)
    355          : ((e, t, a, s) => {
    356              A(e) ||
    357                (C(e, a.class_exited),
    358                ((e, t, a, s) => {
    359                  a.cancel_on_exit &&
    360                    ((e) => E(e) === u)(e) &&
    361                    "IMG" === e.tagName &&
    362                    (te(e),
    363                    ((e) => {
    364                      T(e, (e) => {
    365                        le(e);
    366                      }),
    367                        le(e);
    368                    })(e),
    369                    oe(e),
    370                    O(e, a.class_loading),
    371                    z(s, -1),
    372                    k(e),
    373                    w(a.callback_cancel, e, t, s));
    374                })(e, t, a, s),
    375                w(a.callback_exit, e, t, s));
    376            })(e.target, e, t, a)
    377      );
    378    },
    379    ue = (e) => Array.prototype.slice.call(e),
    380    ge = (e) => e.container.querySelectorAll(e.elements_selector),
    381    be = (e) => ((e) => E(e) === h)(e),
    382    he = (e, t) => ((e) => ue(e).filter(A))(e || ge(t)),
    383    me = function (t, a) {
    384      const s = n(t);
    385      (this._settings = s),
    386        (this.loadingCount = 0),
    387        ((e, t) => {
    388          de(e) ||
    389            (t._observer = new IntersectionObserver((a) => {
    390              _e(a, e, t);
    391            }, ((e) => ({ root: e.container === document ? null : e.container, rootMargin: e.thresholds || e.threshold + "px" }))(e)));
    392        })(s, this),
    393        ((t, a) => {
    394          e &&
    395            ((a._onlineHandler = () => {
    396              ((e, t) => {
    397                var a;
    398                ((a = ge(e)), ue(a).filter(be)).forEach((t) => {
    399                  O(t, e.class_error), k(t);
    400                }),
    401                  t.update();
    402              })(t, a);
    403            }),
    404            window.addEventListener("online", a._onlineHandler));
    405        })(s, this),
    406        this.update(a);
    407    };
    408  return (
    409    (me.prototype = {
    410      update: function (e) {
    411        const a = this._settings,
    412          s = he(e, a);
    413        var n, l;
    414        N(this, s.length),
    415          t
    416            ? this.loadAll(s)
    417            : de(a)
    418            ? ((e, t, a) => {
    419                e.forEach((e) => {
    420                  -1 !== ce.indexOf(e.tagName) &&
    421                    ((e, t, a) => {
    422                      e.setAttribute("loading", "lazy"),
    423                        se(e, t, a),
    424                        ((e, t) => {
    425                          const a = Q[e.tagName];
    426                          a && a(e, t);
    427                        })(e, t),
    428                        I(e, m);
    429                    })(e, t, a);
    430                }),
    431                  N(a, 0);
    432              })(s, a, this)
    433            : ((l = s),
    434              ((e) => {
    435                e.disconnect();
    436              })((n = this._observer)),
    437              ((e, t) => {
    438                t.forEach((t) => {
    439                  e.observe(t);
    440                });
    441              })(n, l));
    442      },
    443      destroy: function () {
    444        this._observer && this._observer.disconnect(),
    445          e && window.removeEventListener("online", this._onlineHandler),
    446          ge(this._settings).forEach((e) => {
    447            S(e);
    448          }),
    449          delete this._observer,
    450          delete this._settings,
    451          delete this._onlineHandler,
    452          delete this.loadingCount,
    453          delete this.toLoadCount;
    454      },
    455      loadAll: function (e) {
    456        const t = this._settings;
    457        he(e, t).forEach((e) => {
    458          M(e, this), ne(e, t, this);
    459        });
    460      },
    461      restoreAll: function () {
    462        const e = this._settings;
    463        ge(e).forEach((t) => {
    464          ie(t, e);
    465        });
    466      },
    467    }),
    468    (me.load = (e, t) => {
    469      const a = n(t);
    470      ne(e, a);
    471    }),
    472    (me.resetStatus = (e) => {
    473      k(e);
    474    }),
    475    e &&
    476      ((e, t) => {
    477        if (t)
    478          if (t.length) for (let a, s = 0; (a = t[s]); s += 1) l(e, a);
    479          else l(e, t);
    480      })(me, window.lazyLoadOptions),
    481    me
    482  );
    483})();
  • 存放友链的地方 assets/data/links.json

     1[
     2  {
     3    "class_name": "组一",
     4    "class_desc": "组一的描述。",
     5    "link_list": [
     6      {
     7        "name": "A",
     8        "bio": "A的简介。",
     9        "url": "https://a.example.com",
    10        "avatar": "https://a.example.com/avatar.jpg"
    11      },
    12      {
    13        "name": "B",
    14        "bio": "B的简介。",
    15        "url": "https://b.example.com",
    16        "avatar": "https://b.example.com/avatar.jpg"
    17      }
    18    ]
    19  },
    20  {
    21    "class_name": "组二",
    22    "class_desc": "组二的描述。",
    23    "link_list": [
    24      {
    25        "name": "C",
    26        "bio": "C的简介。",
    27        "url": "https://c.example.com",
    28        "avatar": "https://c.example.com/avatar.jpg"
    29      }
    30    ]
    31  }
    32]
  • 对应的 css 效果

     1/* 友链 */
     2.friend-avatar-wrapper {
     3  width: 64px;
     4  aspect-ratio: 1 / 1; /* 保证是正方形,避免压扁 */
     5  border-radius: 50%;     /* 圆形裁切 */
     6  overflow: hidden;
     7  border: 2px solid #9ca3af;
     8  display: flex;
     9  align-items: center;
    10  justify-content: center;
    11  background-color: white;
    12  box-sizing: border-box;
    13  transition: transform 0.6s ease;
    14}
    15
    16/* dark 模式下边框稍调亮 */
    17html.dark .friend-avatar-wrapper {
    18  border-color: #d1d5db;  /* light gray for dark mode */
    19}
    20
    21.friend-avatar {
    22  width: 100%;
    23  height: 100%;
    24  object-fit: cover;
    25  display: block;
    26}
    27
    28/* 友链卡片悬停效果 */
    29.friend-card {
    30  transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
    31}
    32
    33.friend-card:hover {
    34  transform: scale(1.05);
    35  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    36}
  • 在对应的 Front Matter 中指定 layout: "links" 即可调用这个模版进行渲染

  • 额外新增了手动指定 json 数据文件路径的 Front Matter 字段 linksFile,增加了一点点灵活性

    • 只要在对应的 Front Matter 中添加 linksFile: "data/xxxx.json" 字段即可使用特定的数据

卡片样式修改
#

  • 参考友链情况,修改了文章卡片的样式

  • 复制 themes/blowfish/layouts/partials/article-link/card.htmllayouts/partials/article-link/card.html 稍微进行了调整

      1{{/* Used by
      2  1. list.html and term.html (when the cardView option is enabled)
      3  2. Recent articles template (when the cardView option is enabled)
      4  3. Shortcode list.html
      5*/}}
      6{{ $disableImageOptimization := site.Store.Get "disableImageOptimization" }}
      7
      8{{ $page := .Page }}
      9{{ $featured := "" }}
     10{{ $featuredURL := "" }}
     11{{ if not .Params.hideFeatureImage }}
     12  {{/* frontmatter */}}
     13  {{ with $page }}
     14    {{ with .Params.featureimage }}
     15      {{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
     16        {{ if site.Params.hotlinkFeatureImage }}
     17          {{ $featuredURL = . }}
     18        {{ else }}
     19          {{ $featured = resources.GetRemote . }}
     20        {{ end }}
     21      {{ else }}
     22        {{ $featured = resources.Get . }}
     23      {{ end }}
     24    {{ end }}
     25
     26    {{/* page resources */}}
     27    {{ if not (or $featured $featuredURL) }}
     28      {{ $images := .Resources.ByType "image" }}
     29      {{ range slice "*feature*" "*cover*" "*thumbnail*" }}
     30        {{ if not $featured }}{{ $featured = $images.GetMatch . }}{{ end }}
     31      {{ end }}
     32    {{ end }}
     33
     34    {{/* fallback to default */}}
     35    {{ if not (or $featured $featuredURL) }}
     36      {{ $default := site.Store.Get "defaultFeaturedImage" }}
     37      {{ if $default.url }}
     38        {{ $featuredURL = $default.url }}
     39      {{ else if $default.obj }}
     40        {{ $featured = $default.obj }}
     41      {{ end }}
     42    {{ end }}
     43
     44    {{/* generate image URL if not hotlink */}}
     45    {{ if not $featuredURL }}
     46      {{ with $featured }}
     47        {{ $featuredURL = .RelPermalink }}
     48        {{ if not (or $disableImageOptimization (eq .MediaType.SubType "svg")) }}
     49          {{ $featuredURL = (.Resize "600x").RelPermalink }}
     50        {{ end }}
     51      {{ end }}
     52    {{ end }}
     53  {{ end }}
     54{{ end }}
     55
     56
     57<article
     58  class="friend-card relative min-h-full min-w-full overflow-hidden rounded-xl bg-white shadow-sm border border-neutral-200 dark:bg-neutral-800 dark:border-neutral-700">
     59  {{ with $featuredURL }}
     60    <div class="flex-none relative overflow-hidden thumbnail_card">
     61      <img
     62        src="{{ . }}"
     63        alt="{{ $page.Title | plainify }}"
     64        loading="lazy"
     65        decoding="async"
     66        class="not-prose absolute inset-0 w-full h-full object-cover">
     67    </div>
     68  {{ end }}
     69  {{ if and .Draft .Site.Params.article.showDraftLabel }}
     70    <span class="absolute top-0 right-0 m-2">
     71      {{ partial "badge.html" (i18n "article.draft" | emojify) }}
     72    </span>
     73  {{ end }}
     74  <div class="px-6 py-4">
     75    <header>
     76      <a
     77        {{ with $page.Params.externalUrl }}
     78          href="{{ . }}" target="_blank" rel="external"
     79        {{ else }}
     80          href="{{ $page.RelPermalink }}"
     81        {{ end }}
     82        class="not-prose before:absolute before:inset-0 decoration-primary-500 dark:text-neutral text-xl font-bold text-neutral-800 hover:underline hover:underline-offset-2">
     83        <h2>
     84          {{ .Title | emojify }}
     85          {{ if .Params.externalUrl }}
     86            <span class="cursor-default align-top text-xs text-neutral-400 dark:text-neutral-500">
     87              <span class="rtl:hidden">&#8599;</span>
     88              <span class="ltr:hidden">&#8598;</span>
     89            </span>
     90          {{ end }}
     91        </h2>
     92      </a>
     93    </header>
     94    <div class="text-sm text-neutral-500 dark:text-neutral-400">
     95      {{ partial "article-meta/basic.html" . }}
     96    </div>
     97    {{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
     98      <div class="prose dark:prose-invert py-1">{{ .Summary | plainify }}</div>
     99    {{ end }}
    100  </div>
    101  <div class="px-6 pt-4 pb-2"></div>
    102</article>
  • 复制 themes/blowfish/layouts/partials/article-link/simple.htmllayouts/partials/article-link/simple.html 稍微进行了调整

      1{{/* Used by
      2  1. list.html and term.html (when the cardView option is not enabled)
      3  2. Recent articles template (when the cardView option is not enabled)
      4  3. Shortcode list.html
      5*/}}
      6{{ $constrainItemsWidth := site.Params.list.constrainItemsWidth | default false }}
      7{{ $disableImageOptimization := site.Store.Get "disableImageOptimization" }}
      8
      9{{/* Force card styling as per user request */}}
     10{{ $cardClasses := "friend-card flex flex-col md:flex-row relative bg-white dark:bg-neutral-800 shadow-sm border border-neutral-200 dark:border-neutral-700 rounded-xl overflow-hidden" }}
     11{{ $imgWrapperClasses := "" }}
     12{{ $cardContentClasses := "p-4" }}
     13
     14{{ if $constrainItemsWidth }}
     15  {{ $cardClasses = printf "%s max-w-prose" $cardClasses }}
     16{{ end }}
     17
     18{{ $page := .Page }}
     19{{ $featured := "" }}
     20{{ $featuredURL := "" }}
     21{{ if not .Params.hideFeatureImage }}
     22  {{/* frontmatter */}}
     23  {{ with $page }}
     24    {{ with .Params.featureimage }}
     25      {{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
     26        {{ if site.Params.hotlinkFeatureImage }}
     27          {{ $featuredURL = . }}
     28        {{ else }}
     29          {{ $featured = resources.GetRemote . }}
     30        {{ end }}
     31      {{ else }}
     32        {{ $featured = resources.Get . }}
     33      {{ end }}
     34    {{ end }}
     35
     36    {{/* page resources */}}
     37    {{ if not (or $featured $featuredURL) }}
     38      {{ $images := .Resources.ByType "image" }}
     39      {{ range slice "*feature*" "*cover*" "*thumbnail*" }}
     40        {{ if not $featured }}{{ $featured = $images.GetMatch . }}{{ end }}
     41      {{ end }}
     42    {{ end }}
     43
     44    {{/* fallback to default */}}
     45    {{ if not (or $featured $featuredURL) }}
     46      {{ $default := site.Store.Get "defaultFeaturedImage" }}
     47      {{ if $default.url }}
     48        {{ $featuredURL = $default.url }}
     49      {{ else if $default.obj }}
     50        {{ $featured = $default.obj }}
     51      {{ end }}
     52    {{ end }}
     53
     54    {{/* generate image URL if not hotlink */}}
     55    {{ if not $featuredURL }}
     56      {{ with $featured }}
     57        {{ $featuredURL = .RelPermalink }}
     58        {{ if not (or $disableImageOptimization (eq .MediaType.SubType "svg")) }}
     59          {{ $featuredURL = (.Resize "600x").RelPermalink }}
     60        {{ end }}
     61      {{ end }}
     62    {{ end }}
     63  {{ end }}
     64{{ end }}
     65
     66
     67<article class="{{ $cardClasses }}">
     68  {{ with $featuredURL }}
     69    <div class="flex-none relative overflow-hidden {{ $imgWrapperClasses }} thumbnail">
     70      <img
     71        src="{{ . }}"
     72        alt="{{ $.Params.featuredImageAlt | default ($.Title | emojify) }}"
     73        loading="lazy"
     74        decoding="async"
     75        class="not-prose absolute inset-0 w-full h-full object-cover">
     76    </div>
     77  {{ end }}
     78  <div class="{{ $cardContentClasses }}">
     79    <header class="items-center text-start text-xl font-semibold">
     80      <a
     81        {{ with $page.Params.externalUrl }}
     82          href="{{ . }}" target="_blank" rel="external"
     83        {{ else }}
     84          href="{{ $page.RelPermalink }}"
     85        {{ end }}
     86        class="not-prose before:absolute before:inset-0 decoration-primary-500 dark:text-neutral text-xl font-bold text-neutral-800 hover:underline hover:underline-offset-2">
     87        <h2>
     88          {{ .Title | emojify }}
     89          {{ if .Params.externalUrl }}
     90            <span class="cursor-default align-top text-xs text-neutral-400 dark:text-neutral-500">
     91              <span class="rtl:hidden">&#8599;</span>
     92              <span class="ltr:hidden">&#8598;</span>
     93            </span>
     94          {{ end }}
     95        </h2>
     96      </a>
     97      {{ if and .Draft .Site.Params.article.showDraftLabel }}
     98        <div class="ms-2">{{ partial "badge.html" (i18n "article.draft" | emojify) }}</div>
     99      {{ end }}
    100      {{ if templates.Exists "partials/extend-article-link.html" }}
    101        {{ partial "extend-article-link.html" . }}
    102      {{ end }}
    103    </header>
    104    <div class="text-sm text-neutral-500 dark:text-neutral-400">
    105      {{ partial "article-meta/basic.html" . }}
    106    </div>
    107    {{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
    108      <div class="prose dark:prose-invert max-w-fit py-1">{{ .Summary | plainify }}</div>
    109    {{ end }}
    110  </div>
    111  <div class="px-6 pt-4 pb-2"></div>
    112</article>

字体颜色和一些标记颜色
#

  • 略微调整了一下

     1/* 调整正文字体颜色,使其更白更易读 */
     2.dark .prose {
     3  color: #d1d5db; /* 稍微降低亮度,拉开与加粗的对比 */
     4}
     5
     6.dark .prose strong,
     7.dark .prose h1,
     8.dark .prose h2,
     9.dark .prose h3,
    10.dark .prose h4,
    11.dark .prose h5,
    12.dark .prose h6 {
    13  color: #f2f2f2;
    14}
    15
    16/* 调整列表标记颜色 */
    17.dark .prose ul > li::marker,
    18.dark .prose ol > li::marker{
    19  color: rgb(var(--color-primary-300));
    20}
  • 复制 themes/blowfish/layouts/_default/_markup/render-heading.htmllayouts/_default/_markup/render-heading.html 调整文章小标题鼠标悬浮显示的 # 的颜色

     1{{ $anchor := anchorize .Anchor }}
     2<h{{ .Level }} class="relative group">{{ .Text | safeHTML }}
     3    <div id="{{ $anchor }}" class="anchor"></div>
     4    {{ if .Page.Params.showHeadingAnchors | default (.Page.Site.Params.article.showHeadingAnchors | default true) }}
     5    <span
     6        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
     7        <a class="text-primary-300 dark:text-primary-400 !no-underline" href="#{{ $anchor }}" aria-label="{{ i18n "article.anchor_label" }}">#</a>
     8    </span>
     9    {{ end }}
    10</h{{ .Level }}>

网站永久链接
#

  • Hugo 永久链接
  • 为了保持和之前 Hexo 站点的 URL 内容和结构一致,主要是在 hugo.toml 中设置 permalinks 字段
    1[permalinks]
    2  posts = "/posts/:slug/"
  • 这样就可以把文章的 URL 变成 yoursite.com/posts/your-slug/ 的形式,然后在 archetypes/default.md 中补充一个 slug 字段
    1...
    2slug: {{ substr (md5 (printf "%s%s" .Date (replace .TranslationBaseName "-" " " | title))) 4 8 }}
    3...
  • 这样通过 hugo new posts/xxx/index.md 创建的文章就会自动生成一个基于日期和标题做 md5 的唯一 slug

页脚信息
#

  • 这里包括版权信息、备案信息,还有“苟活”计时器

  • 创建 layouts/partials/footer.html

     1<!-- 省略之前的内容 -->
     2  <div class="flex items-center justify-between">
     3    {{/* Copyright */}}
     4    {{ if .Site.Params.footer.showCopyright | default true }}
     5      <p class="text-sm text-neutral-500 dark:text-neutral-400">
     6        <span class="inline-block beian-container">
     7          <a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://beian.mps.gov.cn/#/query/webSearch?code=" rel="noreferrer" target="_blank">
     8            <img class="nozoom" src="备案图标" style="display:inline-block;vertical-align:sub;margin-right:2px;" width="16" height="16">网站备案号
     9          </a>
    10          <span class="separator mx-1">|</span>
    11          <a href="https://beian.miit.gov.cn/" target="_blank" class="hover:underline hover:decoration-primary-400 hover:text-primary-500">ICP备案号</a>
    12        </span>
    13        <span class="block mt-1">
    14        {{- with replace .Site.Params.copyright "{ year }" now.Year }}
    15          {{ . | markdownify }}
    16        {{- else }}
    17          &copy;
    18          {{ now.Format "2006" }}
    19          {{ .Site.Params.Author.name | markdownify }}
    20        {{- end }}
    21        </span>
    22      </p>
    23    {{ end }}
    24
    25    {{/* Theme attribution */}}
    26    {{ if .Site.Params.footer.showThemeAttribution | default true }}
    27      <div class="text-sm text-neutral-500 dark:text-neutral-400 text-right">
    28        <span id="workboard"></span>
    29        <span class="block mt-1">
    30        {{ $hugo := printf `<a class="hover:underline hover:decoration-primary-400 hover:text-primary-500"
    31          href="https://gohugo.io/" target="_blank" rel="noopener noreferrer">Hugo</a>`
    32        }}
    33        {{ $blowfish := printf `<a class="hover:underline hover:decoration-primary-400 hover:text-primary-500"
    34          href="https://blowfish.page/" target="_blank" rel="noopener noreferrer">Blowfish</a>`
    35        }}
    36        {{ i18n "footer.powered_by" (dict "Hugo" $hugo "Theme" $blowfish) | safeHTML }}
    37        <span class="mx-1">|</span>
    3839        <a class="hover:decoration-primary-400 hover:text-primary-500" href="https://www.dogecloud.com/" target="_blank" rel="noopener noreferrer">
    40          <img class="nozoom" src="多吉云 logo" alt="DogeCloud" style="display:inline-block;vertical-align:middle;height:1em;margin:0 2px;position:relative;top:-2px;">
    41        </a>
    42        提供CDN加速服务
    43        </span>
    44      </div>
    45    {{ end }}
    46  </div>
    47  {{ if not .Site.Params.disableImageZoom | default true }}
    48    <script>
    49      mediumZoom(document.querySelectorAll("img:not(.nozoom)"), {
    50        margin: 24,
    51        background: "rgba(0,0,0,0.5)",
    52        scrollOffset: 0,
    53      });
    54    </script>
    55  {{ end }}
    56  {{ $jsProcess := resources.Get "js/process.js" }}
    57  {{ $jsProcess = $jsProcess | resources.Minify | resources.Fingerprint (.Site.Params.fingerprintAlgorithm | default "sha512") }}
    58  <script
    59    type="text/javascript"
    60    src="{{ $jsProcess.RelPermalink }}"
    61    integrity="{{ $jsProcess.Data.Integrity }}"></script>
    62  {{/* Extend footer - eg. for extra scripts, etc. */}}
    63  {{ if templates.Exists "partials/extend-footer.html" }}
    64    {{ partialCached "extend-footer.html" . }}
    65  {{ end }}
    66
    67  {{/* Hidden SVG source for JS */}}
    68  {{ with resources.Get "icons/heartbeat.svg" }}
    69    <div id="heartbeat-svg-source" style="display:none;">
    70      {{ .Content | safeHTML }}
    71    </div>
    72  {{ end }}
    73
    74  {{/* Foot Timer */}}
    75  {{ $footTimer := resources.Get "js/foot_timer.js" | fingerprint "sha256" }}
    76  <script type="text/javascript" src="{{ $footTimer.RelPermalink }}" integrity="{{ $footTimer.Data.Integrity }}"></script>
    77</footer>
  • “苟活”计时器的 js 代码,创建 assets/js/foot_timer.js,其中 Date("09/04/2022 00:00:00") 为网站上线时间

    1var now=new Date;function createtime(){now.setTime(now.getTime()+1e3);var e=new Date("09/04/2022 00:00:00"),t=(now-e)/1e3/60/60/24,n=Math.floor(t),o=(now-e)/1e3/60/60-24*n,a=Math.floor(o);1==String(a).length&&(a="0"+a);var r=(now-e)/1e3/60-1440*n-60*a,i=Math.floor(r);1==String(i).length&&(i="0"+i);var l=(now-e)/1e3-86400*n-3600*a-60*i,w=Math.round(l);1==String(w).length&&(w="0"+w);let icon="";const iconSource=document.getElementById("heartbeat-svg-source");if(iconSource){icon=iconSource.innerHTML;if(icon.indexOf('id="heartbeat"')===-1){icon=icon.replace('<svg','<svg id="heartbeat"')}}else{icon='<i id="heartbeat" class="fas fa-heartbeat"></i>'}let d=`<div style="font-size:14px;font-weight:bold">自 2022-9 以来,小站苟活了 ${n}${a} 小时 ${i}${w}${icon}</div>`;document.getElementById("workboard")&&(document.getElementById("workboard").innerHTML=d)}console.log(now),setInterval((()=>{createtime()}),1e3);

Not-By-AI Badge
#

  • 在页脚菜单栏末尾添加一个 Not-By-AI 徽章
    footer.html
     1<footer id="site-footer" class="py-10 print:hidden">
     2  {{/* Footer menu */}}
     3  {{ if .Site.Params.footer.showMenu | default true }}
     4    {{ if .Site.Menus.footer }}
     5      {{ $onlyIcon := true }}
     6      {{ range .Site.Menus.footer }}
     7        {{ if .Name }}
     8          {{ $onlyIcon = false }}
     9          {{ break }}
    10        {{ end }}
    11      {{ end }}
    12      {{ $navClass := printf "flex flex-row pb-4 text-base font-medium text-neutral-500 dark:text-neutral-400 %s" (cond $onlyIcon "overflow-x-auto py-2" "") }}
    13      {{ $ulClass := printf "flex list-none %s" (cond $onlyIcon "flex-row" "flex-col sm:flex-row") }}
    14      {{ $liClass := printf "flex mb-1 text-end sm:mb-0 sm:me-7 sm:last:me-0 %s" (cond $onlyIcon "me-4" "") }}
    15      <nav class="{{ $navClass }}">
    16        <ul class="{{ $ulClass }}">
    17          {{ range .Site.Menus.footer }}
    18            <li class=" {{ $liClass }}">
    19              <a
    20                class="decoration-primary-500 hover:underline hover:decoration-2 hover:underline-offset-2 flex items-center"
    21                href="{{ .URL }}"
    22                title="{{ .Title }}">
    23                {{ if .Pre }}
    24                  <span {{ if and .Pre .Name }}class="mr-1"{{ end }}>
    25                    {{ partial "icon.html" .Pre }}
    26                  </span>
    27                {{ end }}
    28                {{ .Name | markdownify }}
    29              </a>
    30            </li>
    31          {{ end }}
    32          <li class="{{ $liClass }}">
    33            <a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://notbyai.fyi/" target="_blank" rel="noopener noreferrer">
    34              <img class="nozoom h-8" src="not-by-AI-badge.png" alt="Written by Human, Not by AI">
    35            </a>
    36          </li>
    37        </ul>
    38      </nav>
    39    {{ end }}
    40  {{ end }}
    41  <!-- 省略后续内容 -->

文章结尾版权信息
#

  • 在文章结尾添加版权信息

     1  {{/* Body */}}
     2  <section class="flex flex-col max-w-full mt-0 prose dark:prose-invert lg:flex-row">
     3    {{ $enableToc := site.Params.article.showTableOfContents | default false }}
     4    {{ $enableToc = .Params.showTableOfContents | default $enableToc }}
     5    {{ $showToc := and $enableToc (in .TableOfContents "<ul") }}
     6    {{ $topClass := cond (hasPrefix site.Params.header.layout "fixed") "lg:top-[140px]" "lg:top-10" }}
     7    {{ if $showToc }}
     8      <div class="order-first lg:ml-auto px-0 lg:order-last lg:ps-8 lg:max-w-2xs">
     9        <div class="toc ps-5 print:hidden lg:sticky {{ $topClass }}">
    10          {{ if $showToc }}
    11            {{ partial "toc.html" . }}
    12          {{ end }}
    13        </div>
    14      </div>
    15    {{ end }}
    16
    17
    18    <div class="min-w-0 min-h-0 max-w-fit">
    19      {{ partial "series/series.html" . }}
    20      <div class="article-content max-w-prose mb-20">
    21        {{ .Content }}
    22        {{ $defaultReplyByEmail := site.Params.replyByEmail }}
    23        {{ $replyByEmail := default $defaultReplyByEmail .Params.replyByEmail }}
    24        {{ if $replyByEmail }}
    25          <strong class="block mt-8">
    26            <a
    27              target="_blank"
    28              class="m-1 rounded bg-neutral-300 p-1.5 text-neutral-700 hover:bg-primary-500 hover:text-neutral dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-primary-400 dark:hover:text-neutral-800"
    29              href="mailto:{{ site.Params.Author.email }}?subject={{ replace (printf "Reply to %s" .Title) "\"" "'" }}">
    30              {{ i18n "article.reply_by_email" | default "Reply by Email" }}
    31            </a>
    32          </strong>
    33        {{ end }}
    34      </div>
    35      {{ if (.Params.showAuthorBottom | default (site.Params.article.showAuthorBottom | default false)) }}
    36        {{ template "SingleAuthor" . }}
    37      {{ end }}
    38      {{ partial "series/series-closed.html" . }}
    39      {{ partial "sharing-links.html" . }}
    40      {{ partial "related.html" . }}
    41    </div>
    42
    43    {{ $translations := .AllTranslations }}
    44    {{ with .File }}
    45      {{ $path := .Path }}
    46      {{ range $translations }}
    47        {{ $lang := print "."  .Lang  ".md" }}
    48        {{ $path = replace $path $lang ".md" }}
    49      {{ end }}
    50      {{ $jsPage := resources.Get "js/page.js" }}
    51      {{ $jsPage = $jsPage | resources.Minify | resources.Fingerprint (site.Params.fingerprintAlgorithm | default "sha512") }}
    52      <script
    53        type="text/javascript"
    54        src="{{ $jsPage.RelPermalink }}"
    55        integrity="{{ $jsPage.Data.Integrity }}"
    56        data-oid="views_{{ $path }}"
    57        data-oid-likes="likes_{{ $path }}"></script>
    58    {{ end }}
    59  </section>
    60
    61  {{/* Footer */}}
    62  <footer class="pt-8 max-w-prose print:hidden">
    63  <!-- 后续内容未调整,省略 -->
  • 对应渲染的 css 代码

     1/* 文章版权信息样式 */
     2.post-copyright {
     3  margin-top: 3rem;
     4  padding: 1rem;
     5  border-left: 4px solid rgb(var(--color-primary-500));
     6  background-color: rgb(var(--color-neutral-200));
     7  border-radius: 0 0.5rem 0.5rem 0;
     8  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
     9  transition: transform 0.3s ease, box-shadow 0.3s ease;
    10}
    11
    12.post-copyright .post-copyright-text {
    13  margin-top: 0;
    14  margin-bottom: 0;
    15}
    16
    17.dark .post-copyright {
    18  background-color: rgb(var(--color-neutral-800));
    19}
    20
    21.post-copyright:hover {
    22  transform: translateY(-2px);
    23  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    24}
    25
    26.post-copyright a {
    27  transition: color 0.2s ease;
    28}
    29
    30.post-copyright a:hover {
    31  color: rgb(var(--color-primary-500));
    32}
  • 通过在文章 Front Matter 中添加 copyright: false 字段可以在文章结尾不显示版权信息,即默认显示

选集列表调整
#

  • 调整一下显示的文字

  • 复制 themes/blowfish/layouts/partials/series/series_base.htmllayouts/partials/series/series_base.html 进行修改

     1{{ if .Params.series }}
     2  <summary
     3    class="py-1 text-lg font-semibold cursor-pointer bg-primary-200 text-neutral-800 -ms-5 ps-5 dark:bg-primary-800 dark:text-neutral-100">
     4    {{ i18n "article.part_of_series" }} - 
     5    {{ index .Params.series 0 }}
     6  </summary>
     7  {{ $seriesName := strings.ToLower (index .Params.series 0) }}
     8  {{ range $post := sort (index .Site.Taxonomies.series $seriesName) "Params.series_order" }}
     9    {{ if eq $post.Permalink $.Page.Permalink }}
    10      <div
    11        class="py-1 border-dotted border-neutral-300 border-s-1 -ms-5 ps-5 dark:border-neutral-600">
    12        {{ i18n "article.part" }} {{ $post.Params.series_order }}:
    13        {{ i18n "article.this_article" }}
    14      </div>
    15    {{ else }}
    16      <div
    17        class="py-1 border-dotted border-neutral-300 border-s-1 -ms-5 ps-5 dark:border-neutral-600">
    18        <a href="{{ $post.RelPermalink }}">
    19          {{ i18n "article.part" }} {{ $post.Params.series_order }}:
    20          {{ $post.Params.title }}
    21        </a>
    22      </div>
    23    {{ end }}
    24  {{ end }}
    25{{ end }}
  • 只渲染在文章开头(去掉了结尾的渲染),只需要新建一个空的 layouts/partials/series/series-closed.html 即可

调整头部导航栏
#

  • 原来头部导航栏的 logo 竟然不支持 url 链接,复制 themes/blowfish/layouts/partials/header/basic.htmllayouts/partials/header/basic.html 进行修改
     1{{/* Logo section */}}
     2{{ define "HeaderLogo" }}
     3  {{ if .Site.Params.Logo }}
     4    {{ $logo := resources.Get .Site.Params.Logo }}
     5    {{ if $logo }}
     6      <div>
     7        <a href="{{ "" | relLangURL }}" class="flex">
     8          <span class="sr-only">{{ .Site.Title | markdownify }}</span>
     9          {{ if eq $logo.MediaType.SubType "svg" }}
    10            <span class="logo object-scale-down object-left nozoom">
    11              {{ $logo.Content | safeHTML }}
    12            </span>
    13          {{ else }}
    14            <img
    15              src="{{ $logo.RelPermalink }}"
    16              width="{{ div $logo.Width 5 }}"
    17              height="{{ div $logo.Height 5 }}"
    18              class="logo max-h-[2rem] max-w-[2rem] object-scale-down object-left nozoom"
    19              alt="">
    20          {{ end }}
    21        </a>
    22      </div>
    23    {{ else }}
    24      <div>
    25        <a href="{{ "" | relLangURL }}" class="flex">
    26          <span class="sr-only">{{ .Site.Title | markdownify }}</span>
    27          <img
    28            src="{{ .Site.Params.Logo }}"
    29            class="logo object-scale-down object-left nozoom"
    30            style="width: 36px; height: 36px;"
    31            alt="">
    32        </a>
    33      </div>
    34    {{ end }}
    35  {{- end }}
    36{{ end }}
    37
    38<!-- 省略中间一些的内容 -->
    39<!-- 调整导航栏的 title -->
    40{{/* ========== Render HTML ========== */}}
    41<div
    42  class="main-menu flex items-center justify-between py-6 md:justify-start gap-x-3 pt-[2px] pr-2 md:pr-4 pb-[3px] pl-0">
    43  {{ template "HeaderLogo" . }}
    44  <div class="flex flex-1 items-center justify-between">
    45    <nav class="flex space-x-3">
    46      {{ if not .Site.Params.disableTextInHeader | default true }}
    47        <a href="{{ "" | relLangURL }}" class="text-l font-bold">
    48          {{ .Site.Title | markdownify }}
    49        </a>
    50      {{ end }}
    51    </nav>
    52    {{ template "HeaderDesktopNavigation" . }}
    53    {{ template "HeaderMobileToolbar" . }}
    54  </div>
    55  {{ template "HeaderMobileNavigation" . }}
    56</div>

代码块配色调整
#

  • 参考 语法高亮
  • 直接在 assets/css/custom.css 中添加
     1/* Base Settings */
     2.chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
     3.chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
     4.chroma .hl { background-color: #e5e5e5; }
     5.chroma .lnt, .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; color: #7f7f7f; }
     6.chroma .line { display: flex; }
     7
     8
     9/* --- Github Dark (Dark Mode) --- */
    10.dark .chroma { color: #e6edf3; }
    11.dark .chroma .hl { background-color: #6e7681; }
    12.dark .chroma .lnt, .dark .chroma .ln { color: #737679; }
    13
    14.dark .chroma .k, .dark .chroma .kd, .dark .chroma .kn, .dark .chroma .kr, .dark .chroma .kt, .dark .chroma .nn { color: #ff7b72; }
    15.dark .chroma .kc, .dark .chroma .kp, .dark .chroma .no, .dark .chroma .nl, .dark .chroma .py, .dark .chroma .nv, .dark .chroma .vc, .dark .chroma .vg, .dark .chroma .vi, .dark .chroma .vm, .dark .chroma .ld, .dark .chroma .sa, .dark .chroma .dl, .dark .chroma .se, .dark .chroma .sh, .dark .chroma .sr { color: #79c0ff; }
    16.dark .chroma .nc, .dark .chroma .ne, .dark .chroma .nf, .dark .chroma .fm, .dark .chroma .gh { color: #d2a8ff; font-weight: bold; }
    17.dark .chroma .nd { color: #d2a8ff; font-weight: bold; }
    18.dark .chroma .ni { color: #ffa657; }
    19.dark .chroma .nt { color: #7ee787; }
    20.dark .chroma .l, .dark .chroma .s, .dark .chroma .sb, .dark .chroma .sc, .dark .chroma .sd, .dark .chroma .s2, .dark .chroma .si, .dark .chroma .sx, .dark .chroma .s1, .dark .chroma .ss, .dark .chroma .m, .dark .chroma .mb, .dark .chroma .mf, .dark .chroma .mh, .dark .chroma .mi, .dark .chroma .il, .dark .chroma .mo { color: #a5d6ff; }
    21.dark .chroma .o, .dark .chroma .ow, .dark .chroma .gt { color: #ff7b72; font-weight: bold; }
    22.dark .chroma .c, .dark .chroma .ch, .dark .chroma .cm, .dark .chroma .c1, .dark .chroma .cs, .dark .chroma .cp, .dark .chroma .cpf, .dark .chroma .go, .dark .chroma .gp { color: #8b949e; }
    23.dark .chroma .gd { color: #ffa198; background-color: #490202; }
    24.dark .chroma .gi { color: #56d364; background-color: #0f5323; }
    25.dark .chroma .w { color: #6e7681; }

图片懒加载占位符
#

  • 刚好复用友链卡片中构建的懒加载 js 代码和顶部阅读进度条中的extend-footer.html
  • extend-footer.html 中的最后添加
    1{{ $lazyloadJS := resources.Get "js/lazyload.iife.min.js" | fingerprint "sha256" }}
    2<script type="text/javascript" src="{{ $lazyloadJS.RelPermalink }}" integrity="{{ $lazyloadJS.Data.Integrity }}"></script>
    3<script>
    4    var lazyLoadInstance = new LazyLoad({
    5        elements_selector: ".lazy"
    6    });
    7</script>
  • 重写 Markdown 图片渲染钩子,新建 layouts/_default/_markup/render-image.html,用于覆盖默认的图片渲染逻辑
  • 新的逻辑是将真实图片地址放在 data-src 和 data-srcset 中,并把 src 属性设置为默认的 loading 图片的 URL
     1{{- define "RenderImageSimple" -}}
     2  {{- $imgObj := .imgObj -}}
     3  {{- $src := .src -}}
     4  {{- $alt := .alt -}}
     5  <img
     6    class="my-0 rounded-md lazy"
     7    loading="lazy"
     8    decoding="async"
     9    fetchpriority="low"
    10    alt="{{ $alt }}"
    11    src="<默认 loading 图片的 URL>"
    12    data-src="{{ $src }}"
    13    {{ with $imgObj -}}
    14      {{ with $imgObj.Width }}width="{{ . }}"{{ end }}
    15      {{ with $imgObj.Height }}height="{{ . }}"{{ end }}
    16    {{- end }}>
    17{{- end -}}
    18
    19{{- define "RenderImageResponsive" -}}
    20  {{- $imgObj := .imgObj -}}
    21  {{- $alt := .alt -}}
    22  {{- $originalWidth := $imgObj.Width -}}
    23
    24  {{- $img800 := $imgObj -}}
    25  {{- $img1280 := $imgObj -}}
    26  {{- if gt $originalWidth 800 -}}
    27    {{- $img800 = $imgObj.Resize "800x" -}}
    28  {{- end -}}
    29  {{- if gt $originalWidth 1280 -}}
    30    {{- $img1280 = $imgObj.Resize "1280x" -}}
    31  {{- end -}}
    32
    33  {{- $srcset := printf "%s 800w, %s 1280w" $img800.RelPermalink $img1280.RelPermalink -}}
    34
    35  <img
    36    class="my-0 rounded-md lazy"
    37    loading="lazy"
    38    decoding="async"
    39    fetchpriority="auto"
    40    alt="{{ $alt }}"
    41    {{ with $imgObj.Width }}width="{{ . }}"{{ end }}
    42    {{ with $imgObj.Height }}height="{{ . }}"{{ end }}
    43    src="<默认 loading 图片的 URL>"
    44    data-src="{{ $img800.RelPermalink }}"
    45    data-srcset="{{ $srcset }}"
    46    sizes="(min-width: 768px) 50vw, 65vw"
    47    data-zoom-src="{{ $imgObj.RelPermalink }}">
    48{{- end -}}
    49
    50{{- define "RenderImageCaption" -}}
    51  {{- with .caption -}}
    52    <figcaption>{{ . | markdownify }}</figcaption>
    53  {{- end -}}
    54{{- end -}}
    55
    56{{- $disableImageOptimizationMD := .Page.Site.Params.disableImageOptimizationMD | default false -}}
    57{{- $urlStr := .Destination | safeURL -}}
    58{{- $url := urls.Parse $urlStr -}}
    59{{- $altText := .Text -}}
    60{{- $caption := .Title -}}
    61{{- $isRemote := findRE "^(https?|data)" $url.Scheme -}}
    62{{- $resource := "" -}}
    63
    64{{- if not $isRemote -}}
    65  {{- $resource = or ($.Page.Resources.GetMatch $urlStr) (resources.Get $urlStr) -}}
    66{{- end -}}
    67
    68<figure
    69  {{- range $k, $v := .Attributes -}}
    70    {{- if $v -}}
    71      {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
    72    {{- end -}}
    73  {{- end -}}>
    74  {{- if $isRemote -}}
    75    {{- template "RenderImageSimple" (dict "imgObj" "" "src" $urlStr "alt" $altText) -}}
    76  {{- else if $resource -}}
    77    {{- $isSVG := eq $resource.MediaType.SubType "svg" -}}
    78    {{- $shouldOptimize := and (not $disableImageOptimizationMD) (not $isSVG) -}}
    79    {{- if $shouldOptimize -}}
    80      {{- template "RenderImageResponsive" (dict "imgObj" $resource "alt" $altText) -}}
    81    {{- else -}}
    82      {{/* Not optimize image
    83        If it is an SVG file, pass the permalink
    84        Otherwise, pass the resource to allow width and height attributes
    85      */}}
    86      {{- if $isSVG -}}
    87        {{- template "RenderImageSimple" (dict "imgObj" "" "src" $resource.RelPermalink "alt" $altText) -}}
    88      {{- else -}}
    89        {{- template "RenderImageSimple" (dict "imgObj" $resource "src" $resource.RelPermalink "alt" $altText) -}}
    90      {{- end -}}
    91    {{- end -}}
    92  {{- else -}}
    93    {{- template "RenderImageSimple" (dict "imgObj" "" "src" $urlStr "alt" $altText) -}}
    94  {{- end -}}
    95
    96  {{- template "RenderImageCaption" (dict "caption" $caption) -}}
    97</figure>
  • 重写 Figure 的 shortcode,新建 layouts/shortcodes/figure.html
     1{{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }}
     2{{ if .Get "default" }}
     3  {{ partial "hugo-embedded/shortcodes/figure-default.html" . }}
     4{{ else }}
     5  {{- $url := urls.Parse (.Get "src") }}
     6  {{- $altText := .Get "alt" }}
     7  {{- $caption := .Get "caption" }}
     8  {{- $href := .Get "href" }}
     9  {{- $class := .Get "class" }}
    10  {{- $target := .Get "target" | default "_blank" }}
    11  {{- $nozoom := .Get "nozoom" | default false -}}
    12
    13  <figure>
    14  {{- with $href }}<a href="{{ . }}" {{ with $target }}target="{{ . }}"{{ end }} class="inline-block">{{ end -}}
    15  {{- if findRE "^https?" $url.Scheme }}
    16      <img class="my-0 rounded-md lazy{{ with $nozoom }} nozoom{{ end }}{{ with $class }} {{ . }}{{ end }}" src="<默认 loading 图片的 URL>" data-src="{{ $url.String }}" alt="{{ $altText }}" />
    17  {{- else }}
    18    {{- $resource := "" }}
    19    {{- if $.Page.Resources.GetMatch ($url.String) }}
    20      {{- $resource = $.Page.Resources.GetMatch ($url.String) }}
    21    {{- else if resources.GetMatch ($url.String) }}
    22      {{- $resource = resources.Get ($url.String) }}
    23    {{- end }}
    24    {{- with $resource }}
    25      {{- if or $disableImageOptimization (eq .MediaType.SubType "svg")}}
    26        <img
    27          class="my-0 rounded-md lazy{{ with $nozoom }} nozoom{{ end }}{{ with $class }} {{ . }}{{ end }}"
    28          src="<默认 loading 图片的 URL>"
    29          data-src="{{ .RelPermalink }}"
    30          alt="{{ $altText }}"
    31        />
    32      {{- else }}
    33        <img
    34          class="my-0 rounded-md lazy{{ with $nozoom }} nozoom{{ end }}{{ with $class }} {{ . }}{{ end }}"
    35          loading="lazy"
    36          decoding="async"
    37          fetchpriority="auto"
    38          alt="{{ $altText }}"
    39          {{ with .Width }}width="{{ . }}"{{ end }}
    40          {{ with .Height }}height="{{ . }}"{{ end }}
    41          src="<默认 loading 图片的 URL>"
    42          data-src="{{ (.Resize "800x").RelPermalink }}"
    43          data-srcset="
    44          {{- (.Resize "800x").RelPermalink }} 800w,
    45          {{- (.Resize "1280x").RelPermalink }} 1280w"
    46          sizes="(min-width: 768px) 50vw, 65vw"
    47          data-zoom-src="{{ .RelPermalink }}"
    48        />
    49      {{- end }}
    50    {{- else }}
    51      <img class="my-0 rounded-md lazy{{ with $nozoom }} nozoom{{ end }}{{ with $class }} {{ . }}{{ end }}" src="<默认 loading 图片的 URL>" data-src="{{ $url.String }}" alt="{{ $altText }}" />
    52    {{- end }}
    53  {{- end }}
    54  {{ if $href }}</a>{{ end }}
    55  {{ with $caption }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
    56  </figure>
    57{{- end -}}

本文作者: SuburbiaXX
本文链接: https://suburbiaxx.fun/posts/f6af86d0/
版权声明: 本博客在未特别注明下默认使用 CC BY-NC-SA 4.0 许可协议。