跳过正文
  1. 文章/

Blowfish 主题魔改

·8062 字·17 分钟
SuburbiaXX
作者
SuburbiaXX
Life is full of regrets.
目录

前言
#

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

参考魔改
#

主题色
#

  • 参考上述文章进行了稍微调整,直接在 assets/css/custom.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
    
    :root {
      --color-neutral: 255,255,255;
      --color-neutral-50:     250,250,249;
      --color-neutral-100:    245,245,244;
      --color-neutral-200:    231,229,228;
      --color-neutral-300:    214,211,209;
      --color-neutral-400:    168,162,158;
      --color-neutral-500:    120,113,108;
      --color-neutral-600:    87,83,78;
      --color-neutral-700:    68,64,60;
      --color-neutral-800:    41,37,36;
      --color-neutral-900:    28,25,23;
    
      --color-primary-50:     255,255,255;
      --color-primary-100:    255,255,255;
      --color-primary-200:    255,255,255;
      --color-primary-300:    242,211,223;
      --color-primary-400:    225,151,181;
      --color-primary-500:    208,92,138;
      --color-primary-600:    199,60,115;
      --color-primary-700:    170,49,97;
      --color-primary-800:    138,40,79;
      --color-primary-900:    106,31,61;
    
      --color-secondary-50:     255,255,255;
      --color-secondary-100:    255,255,255;
      --color-secondary-200:    255,255,255;
      --color-secondary-300:    255,242,219;
      --color-secondary-400:    255,215,143;
      --color-secondary-500:    255,188,66;
      --color-secondary-600:    255,174,25;
      --color-secondary-700:    239,155,0;
      --color-secondary-800:    199,128,0;
      --color-secondary-900:    158,102,0;
    }

标题鼠标悬停
#

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

文章目录添加悬停样式
#

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

顶部阅读进度条
#

  • css 样式代码

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    <!-- 进度条逻辑 -->
    <script>
        window.addEventListener('scroll', () => {
          const bar = document.querySelector('.top-scroll-bar');
          if (!bar) return;
          const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
          const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
          const width = (scrollTop / scrollHeight) * 100;
          bar.style.width = width + '%';
          bar.style.display = 'block';
        });
    </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
     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
    
    {{ define "main" }}
      {{ .Scratch.Set "scope" "single" }}
      <article>
        {{/* Hero */}}
        {{ if .Params.showHero | default (site.Params.article.showHero | default false) }}
          {{ $heroStyle := .Params.heroStyle }}
          {{ if not $heroStyle }}{{ $heroStyle = site.Params.article.heroStyle }}{{ end }}
          {{ $heroStyle := print "hero/" $heroStyle ".html" }}
          {{ if templates.Exists ( printf "partials/%s" $heroStyle ) }}
            {{ partial $heroStyle . }}
          {{ else }}
            {{ partial "hero/basic.html" . }}
          {{ end }}
        {{ end }}
        <!-- 文章密码功能 -->
        {{ if ( .Params.password | default "" ) }}
        <script>
            (function(){
                if (prompt('请输入文章密码') != "{{ .Params.password }}"){
                    alert('密码错误!');
                    if (history.length === 1) {
                        window.opener = null;
                        window.open('', '_self');
                        window.close();
                    } else {
                        history.back();
                    }
                }
            })();
        </script>
        {{ end }}
        <!-- 文章密码功能结束 -->
  • 需要加密码的文章只需要在 Front Matter 中添加 password: xxx 字段即可

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

  • layouts/shortcodes 新建 blur.html

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

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

友链卡片
#

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

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

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

      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
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    
    {{ define "main" }}
    {{ .Scratch.Set "scope" "single" }}
    <article>
        {{ if .Params.showHero | default (.Site.Params.article.showHero | default false) }}
        {{ $heroStyle := .Params.heroStyle }}
        {{ if not $heroStyle }}{{ $heroStyle = .Site.Params.article.heroStyle }}{{ end }}
        {{ $heroStyle := print "hero/" $heroStyle ".html" }}
      {{ if templates.Exists ( printf "partials/%s" $heroStyle ) }}
        {{ partial $heroStyle . }}
        {{ else }}
        {{ partial "hero/basic.html" . }}
        {{ end }}
        {{ end }}
    
        <header id="single_header" class="mt-5 max-w-prose">
            {{ if .Params.showBreadcrumbs | default (.Site.Params.article.showBreadcrumbs | default false) }}
            {{ partial "breadcrumbs.html" . }}
            {{ end }}
            <h1 class="mt-0 text-4xl font-extrabold text-neutral-900 dark:text-neutral">
                {{ .Title }}
            </h1>
    
            <div class="mt-1 mb-6 text-base text-neutral-500 dark:text-neutral-400 print:hidden">
                {{ partial "article-meta/basic.html" (dict "context" . "scope" "single") }}
            </div>
    
        </header>
    
        <section class="flex flex-col max-w-full mt-0 prose dark:prose-invert dark:marker:text-neutral-400 lg:flex-row">
    
                <div class="min-w-0 min-h-0 max-w-fit">
    
                    <div class="article-content max-w-full mb-20 break-words">
    
                            {{ .Content }}
    
                            <!-- 添加 not-prose 类来避免 prose 样式的影响 -->
                            <div class="not-prose">
                                    {{ $dataFile := .Params.linksFile }}
                                    {{ if not $dataFile }}
                                        {{ errorf "linksFile param is missing in front matter for page %s" .File.Path }}
                                    {{ end }}
                                    {{ $linksResource := resources.Get $dataFile }}
                                    {{ if $linksResource }}
                                        {{ $data := $linksResource | transform.Unmarshal }}
    
                                        {{/* Determine if we are dealing with a list of categories or a single object with a 'links' key */}}
                                        {{ $categories := slice }}
    
                                        {{ if reflect.IsSlice $data }}
                                            {{ $categories = $data }}
                                        {{ else if reflect.IsMap $data }}
                                            {{ if isset $data "links" }}
                                                {{/* Convert old format to new format structure for consistent rendering */}}
                                                {{ $categories = slice (dict "link_list" $data.links) }}
                                            {{ end }}
                                        {{ end }}
    
                                        {{ range $categories }}
                                            {{ if .class_name }}
                                                <h2 class="mt-8 mb-4 text-2xl font-bold text-neutral-900 dark:text-neutral-100 flex items-center">
                                                    {{ if .class_icon }}<i class="{{ .class_icon }} mr-2"></i>{{ end }}
                                                    {{ .class_name }}
                                                </h2>
                                            {{ end }}
                                            {{ if .class_desc }}
                                                <p class="mb-4 text-neutral-600 dark:text-neutral-400">{{ .class_desc }}</p>
                                            {{ else }}
                                                <p class="mb-4 text-neutral-600 dark:text-neutral-400">{{ "&nbsp;" | safeHTML }}</p>
                                            {{ end }}
    
                                            <div class="friends-links grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
                                                {{ range .link_list }}
                                                {{ $name := .name }}
                                                {{ $bio := .bio | default .descr }}
                                                {{ $url := .url | default .link }}
                                                {{ $avatar := .avatar }}
    
                                                <a target="_blank" href="{{ $url }}" title="{{ $name }}" class="block h-full" rel="noopener noreferrer">
                                                    <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">
                                                    <div class="flex-shrink-0 mr-4">
                                                        <div class="friend-avatar-wrapper">
                                                        <img class="friend-avatar lazy nozoom w-16 h-16 object-cover rounded-full block" loading="lazy"
                                                            src="{{ $avatar }}"
                                                            alt="{{ $name }}"
                                                            onerror="this.onerror=null; this.src='img/default.jpg';"
                                                            style="width: 64px; height: 64px;">
                                                        </div>
                                                    </div>
                                                    <span class="inline-block w-4 h-4 mr-3"></span>
                                                    <div class="flex-grow min-w-0">
                                                        <div class="flex items-center mb-2">
                                                        <span class="font-medium text-neutral-900 dark:text-neutral-100 truncate" style="strong">{{ $name }}</span>
                                                        </div>
                                                        <p class="text-sm text-neutral-700 dark:text-neutral-300 line-clamp-2 m-0">{{ $bio }}</p>
                                                    </div>
                                                    </div>
                                                </a>
                                                {{ end }}
                                            </div>
                                        {{ end }}
                                    {{ else }}
                                        {{ errorf "Could not find data file: %s" $dataFile }}
                                    {{ end }}
                            </div>
    
                    </div>
    
                </div>
    
            </section>
    
    </article>
    
    {{ $lazyloadJS := resources.Get "js/lazyload.iife.min.js" | fingerprint "sha256" }}
    <script type="text/javascript" src="{{ $lazyloadJS.RelPermalink }}" integrity="{{ $lazyloadJS.Data.Integrity }}"></script>
    
    <script>
            // Random links
            function shuffleArray(array) {
                    for (let i = array.length - 1; i > 0; i--) {
                            const j = Math.floor(Math.random() * (i + 1));
                            [array[i], array[j]] = [array[j], array[i]];
                    }
                    return array;
            }
    
            function randomizeLinks() {
                    const linkContainers = document.querySelectorAll('.friends-links');
    
                    linkContainers.forEach(container => {
                            const links = Array.from(container.querySelectorAll('a'));
                            const shuffledLinks = shuffleArray(links);
    
                            links.forEach(link => link.remove());
    
                            shuffledLinks.forEach(link => container.appendChild(link));
                    });
    
                    // Equalize heights after shuffling
                    setTimeout(equalizeHeights, 0);
            }
    
            function equalizeHeights() {
                    const cards = document.querySelectorAll('.friend-card');
                    let maxHeight = 0;
    
                    // Reset height to auto to get natural height
                    cards.forEach(card => {
                            card.style.height = 'auto';
                    });
    
                    // Find max height
                    cards.forEach(card => {
                            if (card.offsetHeight > maxHeight) {
                                    maxHeight = card.offsetHeight;
                            }
                    });
    
                    // Apply max height to all cards
                    cards.forEach(card => {
                            card.style.height = maxHeight + 'px';
                    });
            }
    
            randomizeLinks();
    
            // Re-calculate on window resize
            window.addEventListener('resize', equalizeHeights);
    </script>
    
    <script>
            var lazyLoadInstance = new LazyLoad({
                    // Your custom settings go here
            });
    </script>
    {{ end }}
  • 新建一个图片懒加载的 js 代码 assets/js/lazyload.iife.min.js

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

     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
    
    [
      {
        "class_name": "组一",
        "class_desc": "组一的描述。",
        "link_list": [
          {
            "name": "A",
            "bio": "A的简介。",
            "url": "https://a.example.com",
            "avatar": "https://a.example.com/avatar.jpg"
          },
          {
            "name": "B",
            "bio": "B的简介。",
            "url": "https://b.example.com",
            "avatar": "https://b.example.com/avatar.jpg"
          }
        ]
      },
      {
        "class_name": "组二",
        "class_desc": "组二的描述。",
        "link_list": [
          {
            "name": "C",
            "bio": "C的简介。",
            "url": "https://c.example.com",
            "avatar": "https://c.example.com/avatar.jpg"
          }
        ]
      }
    ]
  • 对应的 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
    
    /* 友链 */
    .friend-avatar-wrapper {
      width: 64px;
      aspect-ratio: 1 / 1; /* 保证是正方形,避免压扁 */
      border-radius: 50%;     /* 圆形裁切 */
      overflow: hidden;
      border: 2px solid #9ca3af;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: white;
      box-sizing: border-box;
      transition: transform 0.6s ease;
    }
    
    /* dark 模式下边框稍调亮 */
    html.dark .friend-avatar-wrapper {
      border-color: #d1d5db;  /* light gray for dark mode */
    }
    
    .friend-avatar {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }
    
    /* 友链卡片悬停效果 */
    .friend-card {
      transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
    }
    
    .friend-card:hover {
      transform: scale(1.05);
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    }
  • 在对应的 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
      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
    
    {{/* Used by
      1. list.html and term.html (when the cardView option is enabled)
      2. Recent articles template (when the cardView option is enabled)
      3. Shortcode list.html
    */}}
    {{ $disableImageOptimization := site.Store.Get "disableImageOptimization" }}
    
    {{ $page := .Page }}
    {{ $featured := "" }}
    {{ $featuredURL := "" }}
    {{ if not .Params.hideFeatureImage }}
      {{/* frontmatter */}}
      {{ with $page }}
        {{ with .Params.featureimage }}
          {{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
            {{ if site.Params.hotlinkFeatureImage }}
              {{ $featuredURL = . }}
            {{ else }}
              {{ $featured = resources.GetRemote . }}
            {{ end }}
          {{ else }}
            {{ $featured = resources.Get . }}
          {{ end }}
        {{ end }}
    
        {{/* page resources */}}
        {{ if not (or $featured $featuredURL) }}
          {{ $images := .Resources.ByType "image" }}
          {{ range slice "*feature*" "*cover*" "*thumbnail*" }}
            {{ if not $featured }}{{ $featured = $images.GetMatch . }}{{ end }}
          {{ end }}
        {{ end }}
    
        {{/* fallback to default */}}
        {{ if not (or $featured $featuredURL) }}
          {{ $default := site.Store.Get "defaultFeaturedImage" }}
          {{ if $default.url }}
            {{ $featuredURL = $default.url }}
          {{ else if $default.obj }}
            {{ $featured = $default.obj }}
          {{ end }}
        {{ end }}
    
        {{/* generate image URL if not hotlink */}}
        {{ if not $featuredURL }}
          {{ with $featured }}
            {{ $featuredURL = .RelPermalink }}
            {{ if not (or $disableImageOptimization (eq .MediaType.SubType "svg")) }}
              {{ $featuredURL = (.Resize "600x").RelPermalink }}
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
    
    
    <article
      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">
      {{ with $featuredURL }}
        <div class="flex-none relative overflow-hidden thumbnail_card">
          <img
            src="{{ . }}"
            alt="{{ $page.Title | plainify }}"
            loading="lazy"
            decoding="async"
            class="not-prose absolute inset-0 w-full h-full object-cover">
        </div>
      {{ end }}
      {{ if and .Draft .Site.Params.article.showDraftLabel }}
        <span class="absolute top-0 right-0 m-2">
          {{ partial "badge.html" (i18n "article.draft" | emojify) }}
        </span>
      {{ end }}
      <div class="px-6 py-4">
        <header>
          <a
            {{ with $page.Params.externalUrl }}
              href="{{ . }}" target="_blank" rel="external"
            {{ else }}
              href="{{ $page.RelPermalink }}"
            {{ end }}
            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">
            <h2>
              {{ .Title | emojify }}
              {{ if .Params.externalUrl }}
                <span class="cursor-default align-top text-xs text-neutral-400 dark:text-neutral-500">
                  <span class="rtl:hidden">&#8599;</span>
                  <span class="ltr:hidden">&#8598;</span>
                </span>
              {{ end }}
            </h2>
          </a>
        </header>
        <div class="text-sm text-neutral-500 dark:text-neutral-400">
          {{ partial "article-meta/basic.html" . }}
        </div>
        {{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
          <div class="prose dark:prose-invert py-1">{{ .Summary | plainify }}</div>
        {{ end }}
      </div>
      <div class="px-6 pt-4 pb-2"></div>
    </article>
  • 复制 themes/blowfish/layouts/partials/article-link/simple.htmllayouts/partials/article-link/simple.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
    
    {{/* Used by
      1. list.html and term.html (when the cardView option is not enabled)
      2. Recent articles template (when the cardView option is not enabled)
      3. Shortcode list.html
    */}}
    {{ $constrainItemsWidth := site.Params.list.constrainItemsWidth | default false }}
    {{ $disableImageOptimization := site.Store.Get "disableImageOptimization" }}
    
    {{/* Force card styling as per user request */}}
    {{ $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" }}
    {{ $imgWrapperClasses := "" }}
    {{ $cardContentClasses := "p-4" }}
    
    {{ if $constrainItemsWidth }}
      {{ $cardClasses = printf "%s max-w-prose" $cardClasses }}
    {{ end }}
    
    {{ $page := .Page }}
    {{ $featured := "" }}
    {{ $featuredURL := "" }}
    {{ if not .Params.hideFeatureImage }}
      {{/* frontmatter */}}
      {{ with $page }}
        {{ with .Params.featureimage }}
          {{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
            {{ if site.Params.hotlinkFeatureImage }}
              {{ $featuredURL = . }}
            {{ else }}
              {{ $featured = resources.GetRemote . }}
            {{ end }}
          {{ else }}
            {{ $featured = resources.Get . }}
          {{ end }}
        {{ end }}
    
        {{/* page resources */}}
        {{ if not (or $featured $featuredURL) }}
          {{ $images := .Resources.ByType "image" }}
          {{ range slice "*feature*" "*cover*" "*thumbnail*" }}
            {{ if not $featured }}{{ $featured = $images.GetMatch . }}{{ end }}
          {{ end }}
        {{ end }}
    
        {{/* fallback to default */}}
        {{ if not (or $featured $featuredURL) }}
          {{ $default := site.Store.Get "defaultFeaturedImage" }}
          {{ if $default.url }}
            {{ $featuredURL = $default.url }}
          {{ else if $default.obj }}
            {{ $featured = $default.obj }}
          {{ end }}
        {{ end }}
    
        {{/* generate image URL if not hotlink */}}
        {{ if not $featuredURL }}
          {{ with $featured }}
            {{ $featuredURL = .RelPermalink }}
            {{ if not (or $disableImageOptimization (eq .MediaType.SubType "svg")) }}
              {{ $featuredURL = (.Resize "600x").RelPermalink }}
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
    
    
    <article class="{{ $cardClasses }}">
      {{ with $featuredURL }}
        <div class="flex-none relative overflow-hidden {{ $imgWrapperClasses }} thumbnail">
          <img
            src="{{ . }}"
            alt="{{ $.Params.featuredImageAlt | default ($.Title | emojify) }}"
            loading="lazy"
            decoding="async"
            class="not-prose absolute inset-0 w-full h-full object-cover">
        </div>
      {{ end }}
      <div class="{{ $cardContentClasses }}">
        <header class="items-center text-start text-xl font-semibold">
          <a
            {{ with $page.Params.externalUrl }}
              href="{{ . }}" target="_blank" rel="external"
            {{ else }}
              href="{{ $page.RelPermalink }}"
            {{ end }}
            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">
            <h2>
              {{ .Title | emojify }}
              {{ if .Params.externalUrl }}
                <span class="cursor-default align-top text-xs text-neutral-400 dark:text-neutral-500">
                  <span class="rtl:hidden">&#8599;</span>
                  <span class="ltr:hidden">&#8598;</span>
                </span>
              {{ end }}
            </h2>
          </a>
          {{ if and .Draft .Site.Params.article.showDraftLabel }}
            <div class="ms-2">{{ partial "badge.html" (i18n "article.draft" | emojify) }}</div>
          {{ end }}
          {{ if templates.Exists "partials/extend-article-link.html" }}
            {{ partial "extend-article-link.html" . }}
          {{ end }}
        </header>
        <div class="text-sm text-neutral-500 dark:text-neutral-400">
          {{ partial "article-meta/basic.html" . }}
        </div>
        {{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
          <div class="prose dark:prose-invert max-w-fit py-1">{{ .Summary | plainify }}</div>
        {{ end }}
      </div>
      <div class="px-6 pt-4 pb-2"></div>
    </article>

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

  • 略微调整了一下

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

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

网站永久链接
#

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

页脚信息
#

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

     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
    
    <!-- 省略之前的内容 -->
      <div class="flex items-center justify-between">
        {{/* Copyright */}}
        {{ if .Site.Params.footer.showCopyright | default true }}
          <p class="text-sm text-neutral-500 dark:text-neutral-400">
            <span class="inline-block beian-container">
              <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">
                <img class="nozoom" src="备案图标" style="display:inline-block;vertical-align:sub;margin-right:2px;" width="16" height="16">网站备案号
              </a>
              <span class="separator mx-1">|</span>
              <a href="https://beian.miit.gov.cn/" target="_blank" class="hover:underline hover:decoration-primary-400 hover:text-primary-500">ICP备案号</a>
            </span>
            <span class="block mt-1">
            {{- with replace .Site.Params.copyright "{ year }" now.Year }}
              {{ . | markdownify }}
            {{- else }}
              &copy;
              {{ now.Format "2006" }}
              {{ .Site.Params.Author.name | markdownify }}
            {{- end }}
            </span>
          </p>
        {{ end }}
    
        {{/* Theme attribution */}}
        {{ if .Site.Params.footer.showThemeAttribution | default true }}
          <div class="text-sm text-neutral-500 dark:text-neutral-400 text-right">
            <span id="workboard"></span>
            <span class="block mt-1">
            {{ $hugo := printf `<a class="hover:underline hover:decoration-primary-400 hover:text-primary-500"
              href="https://gohugo.io/" target="_blank" rel="noopener noreferrer">Hugo</a>`
            }}
            {{ $blowfish := printf `<a class="hover:underline hover:decoration-primary-400 hover:text-primary-500"
              href="https://blowfish.page/" target="_blank" rel="noopener noreferrer">Blowfish</a>`
            }}
            {{ i18n "footer.powered_by" (dict "Hugo" $hugo "Theme" $blowfish) | safeHTML }}
            <span class="mx-1">|</span>
            <a class="hover:decoration-primary-400 hover:text-primary-500" href="https://www.dogecloud.com/" target="_blank" rel="noopener noreferrer">
              <img class="nozoom" src="多吉云 logo" alt="DogeCloud" style="display:inline-block;vertical-align:middle;height:1em;margin:0 2px;position:relative;top:-2px;">
            </a>
            提供CDN加速服务
            </span>
          </div>
        {{ end }}
      </div>
      {{ if not .Site.Params.disableImageZoom | default true }}
        <script>
          mediumZoom(document.querySelectorAll("img:not(.nozoom)"), {
            margin: 24,
            background: "rgba(0,0,0,0.5)",
            scrollOffset: 0,
          });
        </script>
      {{ end }}
      {{ $jsProcess := resources.Get "js/process.js" }}
      {{ $jsProcess = $jsProcess | resources.Minify | resources.Fingerprint (.Site.Params.fingerprintAlgorithm | default "sha512") }}
      <script
        type="text/javascript"
        src="{{ $jsProcess.RelPermalink }}"
        integrity="{{ $jsProcess.Data.Integrity }}"></script>
      {{/* Extend footer - eg. for extra scripts, etc. */}}
      {{ if templates.Exists "partials/extend-footer.html" }}
        {{ partialCached "extend-footer.html" . }}
      {{ end }}
    
      {{/* Hidden SVG source for JS */}}
      {{ with resources.Get "icons/heartbeat.svg" }}
        <div id="heartbeat-svg-source" style="display:none;">
          {{ .Content | safeHTML }}
        </div>
      {{ end }}
    
      {{/* Foot Timer */}}
      {{ $footTimer := resources.Get "js/foot_timer.js" | fingerprint "sha256" }}
      <script type="text/javascript" src="{{ $footTimer.RelPermalink }}" integrity="{{ $footTimer.Data.Integrity }}"></script>
    </footer>
  • “苟活”计时器的 js 代码,创建 assets/js/foot_timer.js,其中 Date("09/04/2022 00:00:00") 为网站上线时间

    1
    
    var 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);

文章结尾版权信息
#

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

     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
    
      {{/* Body */}}
      <section class="flex flex-col max-w-full mt-0 prose dark:prose-invert lg:flex-row">
        {{ $enableToc := site.Params.article.showTableOfContents | default false }}
        {{ $enableToc = .Params.showTableOfContents | default $enableToc }}
        {{ $showToc := and $enableToc (in .TableOfContents "<ul") }}
        {{ $topClass := cond (hasPrefix site.Params.header.layout "fixed") "lg:top-[140px]" "lg:top-10" }}
        {{ if $showToc }}
          <div class="order-first lg:ml-auto px-0 lg:order-last lg:ps-8 lg:max-w-2xs">
            <div class="toc ps-5 print:hidden lg:sticky {{ $topClass }}">
              {{ if $showToc }}
                {{ partial "toc.html" . }}
              {{ end }}
            </div>
          </div>
        {{ end }}
    
    
        <div class="min-w-0 min-h-0 max-w-fit">
          {{ partial "series/series.html" . }}
          <div class="article-content max-w-prose mb-20">
            {{ .Content }}
            {{ $defaultReplyByEmail := site.Params.replyByEmail }}
            {{ $replyByEmail := default $defaultReplyByEmail .Params.replyByEmail }}
            {{ if $replyByEmail }}
              <strong class="block mt-8">
                <a
                  target="_blank"
                  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"
                  href="mailto:{{ site.Params.Author.email }}?subject={{ replace (printf "Reply to %s" .Title) "\"" "'" }}">
                  {{ i18n "article.reply_by_email" | default "Reply by Email" }}
                </a>
              </strong>
            {{ end }}
          </div>
          {{ if (.Params.showAuthorBottom | default (site.Params.article.showAuthorBottom | default false)) }}
            {{ template "SingleAuthor" . }}
          {{ end }}
          {{ partial "series/series-closed.html" . }}
          {{ partial "sharing-links.html" . }}
          {{ partial "related.html" . }}
        </div>
    
        {{ $translations := .AllTranslations }}
        {{ with .File }}
          {{ $path := .Path }}
          {{ range $translations }}
            {{ $lang := print "."  .Lang  ".md" }}
            {{ $path = replace $path $lang ".md" }}
          {{ end }}
          {{ $jsPage := resources.Get "js/page.js" }}
          {{ $jsPage = $jsPage | resources.Minify | resources.Fingerprint (site.Params.fingerprintAlgorithm | default "sha512") }}
          <script
            type="text/javascript"
            src="{{ $jsPage.RelPermalink }}"
            integrity="{{ $jsPage.Data.Integrity }}"
            data-oid="views_{{ $path }}"
            data-oid-likes="likes_{{ $path }}"></script>
        {{ end }}
      </section>
    
      {{/* Footer */}}
      <footer class="pt-8 max-w-prose print:hidden">
      <!-- 后续内容未调整,省略 -->
  • 对应渲染的 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
    
    /* 文章版权信息样式 */
    .post-copyright {
      margin-top: 3rem;
      padding: 1rem;
      border-left: 4px solid rgb(var(--color-primary-500));
      background-color: rgb(var(--color-neutral-200));
      border-radius: 0 0.5rem 0.5rem 0;
      box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
      transition: transform 0.3s ease, box-shadow 0.3s ease;
    }
    
    .post-copyright .post-copyright-text {
      margin-top: 0;
      margin-bottom: 0;
    }
    
    .dark .post-copyright {
      background-color: rgb(var(--color-neutral-800));
    }
    
    .post-copyright:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    }
    
    .post-copyright a {
      transition: color 0.2s ease;
    }
    
    .post-copyright a:hover {
      color: rgb(var(--color-primary-500));
    }
  • 通过在文章 Front Matter 中添加 copyright: false 字段可以在文章结尾不显示版权信息,即默认显示

选集列表调整
#

  • 调整一下显示的文字

  • 复制 themes/blowfish/layouts/partials/series/series_base.htmllayouts/partials/series/series_base.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
    
    {{ if .Params.series }}
      <summary
        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">
        {{ i18n "article.part_of_series" }} - 
        {{ index .Params.series 0 }}
      </summary>
      {{ $seriesName := strings.ToLower (index .Params.series 0) }}
      {{ range $post := sort (index .Site.Taxonomies.series $seriesName) "Params.series_order" }}
        {{ if eq $post.Permalink $.Page.Permalink }}
          <div
            class="py-1 border-dotted border-neutral-300 border-s-1 -ms-5 ps-5 dark:border-neutral-600">
            {{ i18n "article.part" }} {{ $post.Params.series_order }}:
            {{ i18n "article.this_article" }}
          </div>
        {{ else }}
          <div
            class="py-1 border-dotted border-neutral-300 border-s-1 -ms-5 ps-5 dark:border-neutral-600">
            <a href="{{ $post.RelPermalink }}">
              {{ i18n "article.part" }} {{ $post.Params.series_order }}:
              {{ $post.Params.title }}
            </a>
          </div>
        {{ end }}
      {{ end }}
    {{ end }}
  • 只渲染在文章开头(去掉了结尾的渲染),只需要新建一个空的 layouts/partials/series/series-closed.html 即可

调整头部导航栏
#

  • 原来头部导航栏的 logo 竟然不支持 url 链接,复制 themes/blowfish/layouts/partials/header/basic.htmllayouts/partials/header/basic.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
    
    {{/* Logo section */}}
    {{ define "HeaderLogo" }}
      {{ if .Site.Params.Logo }}
        {{ $logo := resources.Get .Site.Params.Logo }}
        {{ if $logo }}
          <div>
            <a href="{{ "" | relLangURL }}" class="flex">
              <span class="sr-only">{{ .Site.Title | markdownify }}</span>
              {{ if eq $logo.MediaType.SubType "svg" }}
                <span class="logo object-scale-down object-left nozoom">
                  {{ $logo.Content | safeHTML }}
                </span>
              {{ else }}
                <img
                  src="{{ $logo.RelPermalink }}"
                  width="{{ div $logo.Width 5 }}"
                  height="{{ div $logo.Height 5 }}"
                  class="logo max-h-[2rem] max-w-[2rem] object-scale-down object-left nozoom"
                  alt="">
              {{ end }}
            </a>
          </div>
        {{ else }}
          <div>
            <a href="{{ "" | relLangURL }}" class="flex">
              <span class="sr-only">{{ .Site.Title | markdownify }}</span>
              <img
                src="{{ .Site.Params.Logo }}"
                class="logo object-scale-down object-left nozoom"
                style="width: 36px; height: 36px;"
                alt="">
            </a>
          </div>
        {{ end }}
      {{- end }}
    {{ end }}
    
    <!-- 省略中间一些的内容 -->
    <!-- 调整导航栏的 title -->
    {{/* ========== Render HTML ========== */}}
    <div
      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">
      {{ template "HeaderLogo" . }}
      <div class="flex flex-1 items-center justify-between">
        <nav class="flex space-x-3">
          {{ if not .Site.Params.disableTextInHeader | default true }}
            <a href="{{ "" | relLangURL }}" class="text-l font-bold">
              {{ .Site.Title | markdownify }}
            </a>
          {{ end }}
        </nav>
        {{ template "HeaderDesktopNavigation" . }}
        {{ template "HeaderMobileToolbar" . }}
      </div>
      {{ template "HeaderMobileNavigation" . }}
    </div>

代码块配色调整
#

  • 参考 语法高亮
  • 直接在 assets/css/custom.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
    
    /* Base Settings */
    .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
    .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
    .chroma .hl { background-color: #e5e5e5; }
    .chroma .lnt, .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; color: #7f7f7f; }
    .chroma .line { display: flex; }
    
    
    /* --- Github Dark (Dark Mode) --- */
    .dark .chroma { color: #e6edf3; }
    .dark .chroma .hl { background-color: #6e7681; }
    .dark .chroma .lnt, .dark .chroma .ln { color: #737679; }
    
    .dark .chroma .k, .dark .chroma .kd, .dark .chroma .kn, .dark .chroma .kr, .dark .chroma .kt, .dark .chroma .nn { color: #ff7b72; }
    .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; }
    .dark .chroma .nc, .dark .chroma .ne, .dark .chroma .nf, .dark .chroma .fm, .dark .chroma .gh { color: #d2a8ff; font-weight: bold; }
    .dark .chroma .nd { color: #d2a8ff; font-weight: bold; }
    .dark .chroma .ni { color: #ffa657; }
    .dark .chroma .nt { color: #7ee787; }
    .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; }
    .dark .chroma .o, .dark .chroma .ow, .dark .chroma .gt { color: #ff7b72; font-weight: bold; }
    .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; }
    .dark .chroma .gd { color: #ffa198; background-color: #490202; }
    .dark .chroma .gi { color: #56d364; background-color: #0f5323; }
    .dark .chroma .w { color: #6e7681; }

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