前言
将杂物间从Mogege主题迁移到Blowfish之后稍微做了一下CSS样式魔改:Hugo | 记录Blowfish主题美化过程。本篇文章用来记录拖了一个月没更新的短代码,以及友链样式。
功能在于精不在于多,我满意就行啦。
文章(代码)较长,按需拿取食用。
Shortcode 短代码
防剧透的文本高斯模糊样式
在layouts/shortcodes
新建blur.html
:
<span class="blur">{{.Inner | markdownify}}</span>
<style> /* 文本高斯模糊 */ .blur { filter: blur(4px); transition: filter 0.3s ease; } .blur:hover { filter: blur(0); }</style>
不受任何主题限制,鼠标悬停或屏幕点击后文字正常显示,且内文本支持 markdown 格式。小心眼睛会晕…
使用方法:
{{< blur >}}想要高斯模糊的文本{{</ blur >}}
NeoDB 书影音页
想要简单地实现一个书影音页,不想添加太多依赖,所以使用了短代码方式实现。缺点是只有在本地浏览和push构建的时候才能刷新即时数据。所以要经常更新博客呀~
通过 API 获取 NeoDB 数据
NeoDB 提供了开放的 API,可以通过 API 获取用户的书影音记录。
该 API 的用法:
- URL 为
https://neodb.social/api/me/shelf
- 需要
Bearer Token
- 指定
Path Variable type
为wishlist
,progress
,complete
中的一个 - (可选) 指定
Query Param category
为book
,movie
,tv
,podcast
,music
,game
,performance
中的一个 - (可选) 指定
Query Param page
为页码 例如https://neodb.social/api/me/shelf/complete?category=book&page=1
获取 Access Token
的方法:NeoDB Developer#How to authorize, 也可以生成 Test Access Token
用于测试。
为了方便使用、封装 TOKEN
信息,我们可以将获取 API 独立部署成一个 Cloudflare Worker。
在 Cloudflare Workers 中创建一个新的 Worker,设置环境变量 NEODB_TOKEN
,值为刚刚获取的 Access Token
。worker.js
内容如下:
const myBearer = NEODB_TOKEN; // Assuming 'NEODB_TOKEN' is set in your Cloudflare Worker's environment variables
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request))})
async function handleRequest(request) { try { console.log(myBearer) const url = new URL(request.url); const category = url.pathname.substring(1);
// Optionally, handle query parameters (e.g., page number) const page = url.searchParams.get('page') || '1'; // Available values : wishlist, progress, complete const type = url.searchParams.get('type') || 'complete';
let dbApiUrl = `https://neodb.social/api/me/shelf/${type}?category=${category}&page=${page}`; const response = await fetch(dbApiUrl, { method: 'get', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${myBearer}` } });
// Check if the response from the API is OK (status code 200-299) if (!response.ok) { throw new Error(`API returned status ${response.status}`); }
// Optionally, modify or just forward the API's response const data = await response.json(); return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, status: response.status });
} catch (error) { // Handle any errors that occurred during the fetch return new Response(error.message, { status: 500 }); }}
通过 Hugo Shortcode 将数据整合到 Hugo 中
在 /layouts/shortcodes
中创建 neodb-page.html
,注意替换你的 Cloudflare Worker URL:
<!--Available categories: book, movie, tv, podcast, music, game, performance-->{{ $category := .Get 0 }}<!--Available types: wishlist, progress, complete-->{{ $type := .Get 1 }}
{{ if eq $type "" }}{{ $type = "complete" }}{{ end }}
{{ $now := now.Unix }}{{ $url := printf "https://your.workers.dev/%s?type=%s&t=%d" $category $type $now }}
{{ $remote := resources.GetRemote $url }}{{ $json := $remote | transform.Unmarshal }}
<div class="item-gallery"> {{ range $value := first 10 $json.data }} {{ $item := $value.item }} <div class="item-card"> <a class="item-card-upper" href="{{ $item.id }}" target="_blank" rel="noreferrer"> <img class="item-cover" src="{{ .item.cover_image_url }}" alt="{{ .item.display_title }}"> </a> {{ if .item.rating }} <div class="rate"> <span><b>{{ .item.rating }}</b>🌟</span> <br> <span class="rating-count"> {{.item.rating_count}}人评分</span> </div> {{ else}} <div class="rate"> <span>暂无🌟</span> <br> <span class="rating-count"> {{.item.rating_count}}人评分</span> </div> {{ end }} <h3 class="item-title">{{ .item.display_title }}</h3> </div> {{ end }}</div>
<style> .item-gallery { display: flex; padding: 0 1rem; overflow-x: scroll; align-items: baseline; }
.item-card { display: flex; flex-direction: column; flex: 0 0 17%; margin: 0 0.5rem 1rem; border-radius: 5px; transition: transform 0.2s; width: 8rem; }
.item-card:hover { transform: translateY(-5px); }
.rate { text-align: center; }
.rating-count { font-size: 0.8rem; color: grey; }
.item-cover { width: 100%; min-height: 3rem; border: 2px solid transparent; }
.item-title { font-size: 1rem; text-align: center; margin: 0; }
</style>
我稍微修改了里面关于即时获取数据的方法,每次在本地hugo server
预览都可以实时抓取到最新数据,反正本地预览也不怕慢就是了。
然后,在页面中使用 shortcode 引用即可:
## 想读的书
{{< neodb-page book wishlist >}}
## 读过的书
{{< neodb-page book complete >}}
示例:Archive
NeoDB 短代码
其实一共做了三版,v1 是抓取个人标记条目的标题,类别,封面,时间,个人评星和个人评价,API是https://neodb.social/api/me/shelf
,需要token
。
v2 是抓取与个人数据完全无关的公共数据,包含标题,封面,类别,总评分,评分人数和条目描述,API是https://neodb.social/api/
,数据是公开的,不需要token
。
我感觉太多余,本着能少则少的想法做了一个整合版 v3,对v1和v2直接进行整合,逻辑比较复杂:
-
主要抓取条目来源:API是
https://neodb.social/api/
,包含标题,封面,总评分(1-10),评分人数,条目描述。 -
当总评分显示为
null
时,将总评分和评分人数更换为个人评星(1-5星,可半星),API是https://neodb.social/api/me/shelf
。 -
如果条目被标记,则显示标记时间和标记类别,比如“在玩”“想读”。如果没有被标记,则不显示。
v3 满足了我的所有需求,无论什么条目都不会报错。layouts/shortcodes/neodb.html
内容:
{{/* Neodb 综合展示短代码(默认公共评分 + 用户状态 + fallback用户评分) */}}
{{ $dbUrl := .Get 0 }}{{ $authToken := "your_token" }}
{{ $match := findRE `neodb\.social\/(book|movie|tv|game|podcast)\/([^\/]+)` $dbUrl }}{{ if not (len $match) }} <p style="text-align:center"><small>无效的 Neodb 链接格式</small></p> {{ return }}{{ end }}
{{ $segments := split (index $match 0) "/" }}{{ $type := index $segments 1 }}{{ $uuid := index $segments 2 }}
{{ $publicApiUrl := printf "https://neodb.social/api/%s/%s?t=%d" $type $uuid (now.Unix) }}{{ $userApiUrl := printf "https://neodb.social/api/me/shelf/item/%s?t=%d" $uuid (now.Unix) }}
{{ $publicData := (resources.GetRemote $publicApiUrl) | transform.Unmarshal }}{{ $category := $publicData.category }}
{{/* 公共评分 */}}{{ $hasPublicRating := false }}{{ $rating := 0.0 }}{{ $ratingCount := 0 }}{{ with $publicData.rating }} {{ $rating = float . }} {{ $hasPublicRating = true }}{{ end }}{{ with $publicData.rating_count }} {{ $ratingCount = . }}{{ end }}
{{/* 用户数据(用于状态标记 和 fallback 星级) */}}{{ $userShelf := dict }}{{ $userRating := 0 }}{{ $useUserStars := false }}{{ $statusText := "" }}
{{ $userOpts := dict "headers" (dict "Authorization" (print "Bearer " $authToken)) }}{{ $userData := (resources.GetRemote $userApiUrl $userOpts) | transform.Unmarshal }}
{{ if and (isset $userData "shelf_type") (ne ($userData.shelf_type) "") }} {{ $userShelf = $userData }}
{{/* 生成状态文本 */}} {{ $prefix := "" }} {{ $suffix := "" }} {{ $action := "" }} {{ if eq $category "book" }} {{ $action = "读" }} {{ else if or (eq $category "tv") (eq $category "movie") }} {{ $action = "看" }} {{ else if or (eq $category "podcast") (eq $category "album") }} {{ $action = "听" }} {{ else if eq $category "game" }} {{ $action = "玩" }} {{ end }}
{{ $shelfType := $userShelf.shelf_type }} {{ if eq $shelfType "wishlist" }} {{ $prefix = "想" }} {{ else if eq $shelfType "complete" }}{{ $suffix = "过" }} {{ else if eq $shelfType "progress" }}{{ $prefix = "在" }} {{ else if eq $shelfType "dropped" }} {{ $prefix = "不" }}{{ $suffix = "了" }} {{ end }}
{{ $statusText = print $prefix $action $suffix }}{{ end }}
{{ with $userData.rating_grade }} {{ $userRating = float . }} {{ if not $hasPublicRating }} {{ $rating = $userRating }} {{ $useUserStars = true }} {{ end }}{{ end }}
<div class="db-card"> <div class="db-card-subject"> <div class="db-card-post"> <img loading="lazy" src="{{ $publicData.cover_image_url }}" alt="{{ $publicData.title }}" style="max-width: 100%; height: auto;"> </div> <div class="db-card-content"> <div class="db-card-title"> <a href="{{ $dbUrl }}" class="cute" target="_blank" rel="noreferrer">{{ $publicData.title }}</a> </div>
<div class="db-card-rating"> <div class="db-card-rating-meta"> {{ if $useUserStars }} {{ $starCount := div (mul $rating 5) 10 }} {{ $fullStars := int $starCount }} {{ $halfStar := 0 }} {{ if (mod $rating 2) }}{{ $halfStar = 1 }}{{ end }} {{ $emptyStars := sub 5 (add $fullStars $halfStar) }} <span class="db-card-rating-stars"> {{- range $i := (seq 1 $fullStars) -}}<i class="fas fa-star"></i>{{- end -}} {{- if eq $halfStar 1 -}}<i class="fa fa-star-half-stroke"></i>{{- end -}} {{- range $i := (seq 1 $emptyStars) -}}<i class="far fa-star"></i>{{- end -}} </span> {{ else }} <span class="db-card-rating-stars"> <span class="allstardark"> <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span> </span> {{ printf "%.1f" $rating }} <i class="fa-solid fa-ranking-star"></i> </span> <!-- <span class="rating_nums">{{ printf "%.1f" $rating }} <i class="fa-solid fa-ranking-star"></i></span> --> {{ if gt $ratingCount 0 }} <span class="rating_count">{{ $ratingCount }} 个评分</span> {{ end }} {{ end }} {{ with $userShelf.created_time }} {{ $markTime := . | time.Format "2006年01月02日" }} <span class="rating_status">{{ $markTime }} {{ $statusText }}</span> {{ end }} </div> </div>
<div class="db-card-description scrollable-description"> {{ $publicData.brief }} </div> </div> </div></div>
<style>.db-card-rating-meta { display: flex; flex-wrap: wrap; gap: 0.8em; align-items: center; font-size: 0.95em;}.db-card-rating-stars { display: inline-flex; gap: 0.15em;}
.scrollable-description { max-height: 100px; overflow-y: auto; padding-right: 0.5em; line-height: 1.5; margin-top: 0.5em;}.scrollable-description::-webkit-scrollbar { width: 6px;}.scrollable-description::-webkit-scrollbar-thumb { background-color: rgba(150, 150, 150, 0.4); border-radius: 4px;}</style>
这里的icon我是用的是免费的Awesome icon v6版本,需要到Awesome官网注册账号。
<script src="https://kit.fontawesome.com/xxxxxxxxx.js" crossorigin="anonymous"></script>
然后将以上内容放入layouts/partials/extend-footer.html
引用图标,js序列号填写自己获取的。
CSS样式static/css/neodb.css
:
/* Neodb card style */.db-card { margin: 0; background: var(--color-codebg); border-radius: 7px; box-shadow: none; font-size: 14px; padding-top: 10px; }
.db-card-subject { display: flex; align-items: flex-start; line-height: 1.6; position: relative; font-size: inherit; /* 继承全局字体大小 */ }
.dark .db-card { background: var(--color-codebg); }
.db-card-content { flex: 1 1 auto; overflow: auto; margin-top: 8px; }
.db-card-post { width: 100px; margin-right: 20px; margin-top: 0px; display: flex; flex: 0 0 auto; }
.db-card-title { padding-top: 8px; /* 轻微调整标题的高度 */ }
.db-card-rating, .db-card-comment, .db-card-cate { font-size: inherit; /* 继承全局字体大小 */ }
.db-card-title { margin-bottom: 3px; color: #fff; font-weight: bold; }
.db-card-title a { text-decoration: none !important; }
.db-card-comment { margin-top: 0px; color: var(--card-text-color-main); max-height: none; overflow: visible; }
.db-card-description { margin-top: 0px; color: var(--card-text-color-main); max-height: none; overflow: visible; }
.db-card-cate { position: absolute; top: 0; right: 0; padding: 1px 8px; font-style: italic; border-radius: 0 8px 0 8px; text-transform: capitalize; background: #6b0f0f; color: #fff; }
.db-card-post img { width: 100px !important; height: 150px !important; border-radius: 4px; object-fit: cover; }
@media (max-width: 600px) { .db-card { margin: 0.8rem 0.5rem; } }
css没有做更改,里面有一些v3版已经不用的元素但是前两版还在用,所以就放着。
使用方法:
{{< neodb "https://neodb.social/game/72fWr2UHNBhusgSXhEKoio" >}}
适配了PC和手机端显示,条目描述也做了滑动显示。完美!
友情链接样式
借鉴了eallion的代码,缝缝改改自己写了个友链样式,适用于blowfish主题。
layouts/_default/links.html
内容:
{{ 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 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"> <div class="friends-links grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> {{ $linksResource := resources.Get "data/links.json" }} {{ $links := $linksResource | transform.Unmarshal }} {{ $linksResource := resources.Get "data/links.json" }} {{ $links := $linksResource | transform.Unmarshal }}
{{ range $links.links }} {{ $name := .name }} {{ $bio := .bio }} {{ $url := .url }} {{ $avatar := .avatar }}
<a target="_blank" href="{{ $url }}" title="{{ $name }}" class="block" rel="noopener noreferrer"> <div class="group flex items-center p-4 bg-gray-100 dark:bg-gray-900 rounded-lg hover:scale-105 transition-all duration-300 ease-in-out"> <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/Transparent_Akkarin.th.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> </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 linksContainer = document.querySelector('.friends-links'); const links = Array.from(linksContainer.querySelectorAll('a')); const shuffledLinks = shuffleArray(links);
links.forEach(link => link.remove());
shuffledLinks.forEach(link => linksContainer.appendChild(link)); }
randomizeLinks();</script>
<script> var lazyLoadInstance = new LazyLoad({ // Your custom settings go here });</script>
<style>
.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;}
/* 鼠标悬停时旋转 */.friend-avatar-wrapper:hover { transform: rotate(360deg);}
/* 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;}
</style>
{{ end }}
assets/js/lazyload.iife.min.js
,作用是图片懒加载:
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
:
{ "links": [ { "name": "xxx’s Blog", "bio": "个人简介。", "url": "https://example.com/", "avatar": "/img/friends/test.jpg" }, ... ]}
示例:Links
指定了友链头像获取失败时显示的图片:/static/img/Transparent_Akkarin.th.jpg
,选择自己喜欢的图片就好。
在改样式的时候我设置了鼠标悬浮头像旋转,头像宽度高度强制为64px,圆形头像灰色外框(夜间模式更亮),以及白色为底占位,防止一些矢量图如ico
以及不规则形状的图片显示变形。这样无论图片来自远程还是本地,格式大小如何,都能够正常显示。以及友链显示顺序是随机的,看起来会比较有意思吧。