Merge pull request #3 from Ad-closeNN/feat/update-deps

feat: update deps
This commit is contained in:
Ad_closeNN
2026-04-20 10:19:06 +08:00
committed by GitHub
21 changed files with 3317 additions and 3515 deletions
+6
View File
@@ -27,3 +27,9 @@ bun.lockb
yarn.lock
src/content/.obsidian
.playwright-mcp
.serena
.claude
.obsidian
+2
View File
@@ -21,6 +21,7 @@ import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.m
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
import { remarkPublicImagePaths } from "./src/plugins/remark-public-image-paths.mjs";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
import rehypeExternalLinks from 'rehype-external-links';
@@ -108,6 +109,7 @@ export default defineConfig({
remarkMath,
remarkReadingTime,
remarkExcerpt,
remarkPublicImagePaths,
remarkGithubAdmonitionsToDirectives,
remarkDirective,
remarkSectionize,
+26 -26
View File
@@ -16,32 +16,32 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.2",
"@astrojs/svelte": "7.1.0",
"@astrojs/check": "^0.9.8",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "8.0.5",
"@astrojs/tailwind": "^6.0.2",
"@expressive-code/core": "^0.41.3",
"@expressive-code/plugin-collapsible-sections": "^0.41.3",
"@expressive-code/plugin-line-numbers": "^0.41.3",
"@expressive-code/core": "^0.41.7",
"@expressive-code/plugin-collapsible-sections": "^0.41.7",
"@expressive-code/plugin-line-numbers": "^0.41.7",
"@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/fa6-regular": "^1.2.4",
"@iconify-json/fa6-solid": "^1.2.4",
"@iconify-json/ic": "^1.2.2",
"@iconify-json/material-symbols": "^1.2.30",
"@iconify-json/ic": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.67",
"@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.7.0",
"@tailwindcss/typography": "^0.5.16",
"astro": "5.12.8",
"astro-expressive-code": "^0.41.3",
"@swup/astro": "^1.8.0",
"@tailwindcss/typography": "^0.5.19",
"astro": "6.1.8",
"astro-expressive-code": "^0.41.7",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"katex": "^0.16.45",
"markdown-it": "^14.1.1",
"mdast-util-to-string": "^4.0.0",
"node-html-parser": "^7.0.1",
"overlayscrollbars": "^2.11.4",
"pagefind": "^1.3.0",
"node-html-parser": "^7.1.0",
"overlayscrollbars": "^2.15.1",
"pagefind": "^1.5.2",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
@@ -54,22 +54,22 @@
"remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.3",
"sanitize-html": "^2.17.3",
"sharp": "^0.34.5",
"stylus": "^0.64.0",
"svelte": "^5.37.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.2",
"unist-util-visit": "^5.0.0"
"svelte": "^5.55.4",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"@astrojs/ts-plugin": "^1.10.7",
"@biomejs/biome": "2.1.3",
"@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4",
"@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0",
"@types/sanitize-html": "^2.16.1",
"postcss-import": "^16.1.1",
"postcss-nesting": "^13.0.2"
},
+2851 -3355
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@ interface Post {
data: {
title: string;
tags: string[];
category?: string;
category?: string | null;
published: Date;
};
}
+6 -5
View File
@@ -7,6 +7,7 @@ import { i18n } from "../i18n/translation";
import { getDir } from "../utils/url-utils";
import ImageWrapper from "./misc/ImageWrapper.astro";
import PostMetadata from "./PostMeta.astro";
import { render } from 'astro:content';
interface Props {
class?: string;
@@ -20,7 +21,7 @@ interface Props {
image: string;
description: string;
draft: boolean;
style: string;
style?: string;
}
const {
entry,
@@ -32,7 +33,7 @@ const {
category,
image,
description,
style,
style = "",
} = Astro.props;
const className = Astro.props.class;
@@ -40,7 +41,7 @@ const hasCover = image !== undefined && image !== null && image !== "";
const coverWidth = "28%";
const { remarkPluginFrontmatter } = await entry.render();
const { remarkPluginFrontmatter } = await render(entry);
---
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative border border-black/20 dark:border-white/20 hover:scale-[1.02] hover:shadow-xl transition-all duration-[300ms]", className]} style={style}>
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>
@@ -72,7 +73,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
<div>|</div>
<div>
<span class="text-50 text-sm font-medium" id={`page-views-${entry.slug}`}>加载中...</span>
<span class="text-50 text-sm font-medium" id={`page-views-${entry.id}`}>加载中...</span>
</div>
</div>
@@ -110,7 +111,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/PostCard.astro -->
<script define:vars={{ slug: entry.slug }}>
<script define:vars={{ slug: entry.id }}>
// 获取文章浏览量统计
async function fetchPostCardViews(slug) {
const displayElement = document.getElementById(`page-views-${slug}`);
+1 -1
View File
@@ -17,7 +17,7 @@ const interval = 50;
category={entry.data.category}
published={entry.data.published}
updated={entry.data.updated}
url={getPostUrlBySlug(entry.slug)}
url={getPostUrlBySlug(entry.id)}
image={entry.data.image}
description={entry.data.description}
draft={entry.data.draft}
+5 -2
View File
@@ -46,9 +46,12 @@ if (isLocal) {
const imageClass = "w-full h-full object-cover";
const imageStyle = `object-position: ${position}`;
const originalSrc = isLocal && img ? img.src : isPublic ? url(src) : src;
const originalWidth = isLocal && img ? img.width : undefined;
const originalHeight = isLocal && img ? img.height : undefined;
---
<div id={id} class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>}
{!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>}
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-pswp-width={originalWidth} data-pswp-height={originalHeight} data-original-src={originalSrc}/>}
{!isLocal && <img src={originalSrc} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-original-src={originalSrc}/>}
</div>
+53 -31
View File
@@ -3,6 +3,7 @@ import { Icon } from "astro-icon/components";
import { profileConfig } from "../../config";
import { url } from "../../utils/url-utils";
import ImageWrapper from "../misc/ImageWrapper.astro";
import TotalWords from "./TotalWords.astro";
const config = profileConfig;
---
@@ -45,6 +46,8 @@ const config = profileConfig;
</a>}
</div>
<TotalWords />
<!-- 全站访问量统计 https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/widget/Profile.astro -->
<div class="text-center text-sm text-neutral-500 dark:text-neutral-400 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
<div class="flex items-center justify-center gap-1">
@@ -74,18 +77,37 @@ const config = profileConfig;
fetch("https://v1.hitokoto.cn")
.then(response => response.json())
.then(data => {
// API 返回的字段是 data.text
document.getElementById("hitokoto").innerText = data.hitokoto;
const hitokotoElement = document.getElementById("hitokoto");
if (hitokotoElement) {
hitokotoElement.innerText = data.hitokoto;
}
})
.catch(error => {
console.error("获取一言失败:", error);
//document.getElementById("hitokoto").innerText = "获取失败,请稍后再试。";
//失败后用内置的,成功了用别人的。
document.getElementById("hitokoto").innerText = "再热情的心也经不起冷漠,再爱你的人也经不起冷落。";
const hitokotoElement = document.getElementById("hitokoto");
if (hitokotoElement) {
hitokotoElement.innerText = "再热情的心也经不起冷漠,再爱你的人也经不起冷落。";
}
});
</script>
<script>
type SiteStatsData = {
pageviews?: {
value?: number;
};
};
type BlogUmamiStore = {
getStats: (key: string, createUrl: (websiteId: string) => string) => Promise<SiteStatsData | undefined>;
};
declare global {
interface Window {
__blogUmami?: BlogUmamiStore;
}
}
// 获取全站访问量统计
async function loadSiteStats() {
const statsElement = document.getElementById('site-stats');
@@ -96,8 +118,8 @@ fetch("https://v1.hitokoto.cn")
statsElement.dataset.umamiState = 'loading';
try {
const umamiStore = window['__blogUmami'];
const statsData = await umamiStore?.getStats('site:all', (websiteId) => {
const umamiStore = window.__blogUmami;
const statsData = await umamiStore?.getStats('site:all', (websiteId: string) => {
const currentTimestamp = Date.now();
return `https://umami.adclosenn.top/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent('Asia/Shanghai')}&compare=false`;
});
@@ -125,43 +147,43 @@ fetch("https://v1.hitokoto.cn")
</script>
<!-- 获取 Commit 信息 via API -->
<script>
type GithubCommit = {
sha: string;
html_url: string;
commit: {
message: string;
committer: {
date: string;
};
};
};
async function loadCommitStats() {
try {
const statsElement = document.getElementById('github-commit'); // 查找 id
const link = document.getElementById('github-commit-link'); // 查找 id
const statsElement = document.getElementById('github-commit');
const link = document.getElementById('github-commit-link');
// 第一步:调用 API
const githubResponse = await fetch(`https://api.github.com/repos/Ad-closeNN/blog-fuwari/commits?per_page=1`);
if (!githubResponse.ok) {
throw new Error('获取信息失败');
}
let Data = await githubResponse.json();
Data = Data[0];
// 第二步:获取 Commit 数据
const latestCommit = Data;
const data = (await githubResponse.json()) as GithubCommit[];
const latestCommit = data[0];
const commitData = {
hash: latestCommit.sha.slice(0, 7),
fullHash: latestCommit.sha,
message: latestCommit.commit.message.split('\n')[0],
author: latestCommit.commit.author.name,
date: latestCommit.commit.author.date,
url: latestCommit.html_url
};
if (statsElement) {
statsElement.textContent = `当前提交:${Data.sha.slice(0,7)}`;
if (!latestCommit) {
throw new Error('未获取到提交信息');
}
if (statsElement) {
statsElement.textContent = `当前提交:${latestCommit.sha.slice(0, 7)}`;
}
if (link){
// const gurl = "https://github.com/Ad-closeNN/blog-fuwari/commit/"+Data.sha;
const gurl = "/info/";
link.href = gurl;
link.title = "("+Data.commit.committer.date + ")" + " " + Data.commit.message;
if (link instanceof HTMLAnchorElement) {
const infoUrl = "/info/";
link.href = infoUrl;
link.title = `(${latestCommit.commit.committer.date}) ${latestCommit.commit.message}`;
}
} catch (error) {
console.error('获取 Commit 信息失败:', error);
+15
View File
@@ -0,0 +1,15 @@
---
import { Icon } from "astro-icon/components";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getTotalWords } from "@utils/content-utils";
const totalWords = await getTotalWords();
const formattedTotalWords = totalWords.toLocaleString("zh-CN");
---
<div class="text-center text-sm text-neutral-500 dark:text-neutral-400 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
<div class="flex items-center justify-center gap-1">
<Icon name="material-symbols:article-outline" class="text-base"></Icon>
<span>总计 {formattedTotalWords} {i18n(I18nKey.wordsCount)}</span>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { glob } from "astro/loaders";
const postsCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
showcover: z.boolean().optional().default(true),
customcover: z.string().optional().default(""),
lang: z.string().optional().default(""),
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
const specCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/spec" }),
schema: z.object({
title: z.string().optional(),
published: z.date().optional(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
-37
View File
@@ -1,37 +0,0 @@
import { defineCollection, z } from "astro:content";
const postsCollection = defineCollection({
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
showcover: z.boolean().optional().default(true),
customcover: z.string().optional().default(""),
lang: z.string().optional().default(""),
/* For internal use */
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
/* https://github.com/afoim/fuwari/commit/8b651d5d4e666c520d8fc95e89bf9d0ecf307644#diff-544dcd1cb4d05890db2dcf497052df475216a57683c346216e43133407b7ea58 */
const specCollection = defineCollection({
schema: z.object({
title: z.string().optional(),
published: z.date().optional(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
+2 -2
View File
@@ -8,7 +8,7 @@
::github{repo="afoim/fuwari"}
# 站点分流
经过几个月的测试,**本站**之后将弃用 Netlify 托管,转而使用免费且更强大的 Cloudflare。之前使用过的分流测试版站点已移除。谢谢 Netlify,你好 Cloudflare
经过几个月的测试,**本站**之后将弃用 Netlify 托管,转而使用免费且更强大的 Cloudflare。之前使用过的分流测试版站点已移除,且博客域名已改为 `blog.adclosenn.top`。谢谢 Netlify,你好 Cloudflare
# 关于我
一位住在 [中华人民共和国广西壮族自治区](https://baike.baidu.com/item/%E5%B9%BF%E8%A5%BF%E5%A3%AE%E6%97%8F%E8%87%AA%E6%B2%BB%E5%8C%BA/163178) 的学生。 [me.adclosenn.top](https://me.adclosenn.top)
@@ -16,7 +16,7 @@
## 联系方式
电子邮箱:[admin@adclosenn.top](mailto:admin@adclosenn.top)
Discordhttps://discord.com/users/1068060784300658688
BlueSkyhttps://bsky.app/profile/adclosenn.top
Xhttps://x.com/Ad_closeNN
# 关于本站
## 字体
+29 -3
View File
@@ -1,9 +1,32 @@
import type { AstroIntegration } from "@swup/astro";
export {};
type SwupHookHandler = (...args: unknown[]) => void;
type SwupLike = {
hooks: {
on: (eventName: string, handler: SwupHookHandler) => void;
};
};
type BlogPhotoSwipeState = {
lightbox: {
destroy: () => void;
on: (eventName: string, handler: (...args: unknown[]) => void) => void;
pswp?: {
ui?: {
registerElement: (options: Record<string, unknown>) => void;
};
};
} | null;
hookRegistered: boolean;
pageLoadRegistered: boolean;
};
declare global {
interface Window {
// type from '@swup/astro' is incorrect
swup: AstroIntegration;
swup?: SwupLike;
__blogPhotoSwipe?: BlogPhotoSwipeState;
dataLayer?: unknown[];
pagefind: {
search: (query: string) => Promise<{
results: Array<{
@@ -14,6 +37,9 @@ declare global {
}
}
declare function gtag(...args: unknown[]): void;
declare let dataLayer: unknown[];
interface SearchResult {
url: string;
meta: {
+132 -22
View File
@@ -482,8 +482,8 @@ function showBanner() {
}
// 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发
const animatedElements = document.querySelectorAll('.onload-animation');
animatedElements.forEach(el => {
const animatedElements = document.querySelectorAll<HTMLElement>('.onload-animation');
animatedElements.forEach((el) => {
el.style.animation = 'none';
el.style.opacity = '1';
});
@@ -634,53 +634,163 @@ window.onresize = () => {
<script>
import PhotoSwipeLightbox from "photoswipe/lightbox"
import "photoswipe/style.css"
import "../styles/photoswipe.css"
let lightbox: PhotoSwipeLightbox
let pswp = import("photoswipe")
const zoomTargetSelector = ".custom-md img, #post-cover img"
const pswpModule = import("photoswipe")
type PhotoSwipeState = {
lightbox: PhotoSwipeLightbox | null
hookRegistered: boolean
}
const photoSwipeState = (window.__blogPhotoSwipe ??= {
lightbox: null,
hookRegistered: false,
pageLoadRegistered: false,
}) as PhotoSwipeState & { pageLoadRegistered: boolean }
function getZoomTargets() {
return Array.from(document.querySelectorAll<HTMLImageElement>(zoomTargetSelector))
}
function destroyPhotoSwipe() {
photoSwipeState.lightbox?.destroy()
photoSwipeState.lightbox = null
}
function createPhotoSwipe() {
lightbox = new PhotoSwipeLightbox({
gallery: ".custom-md img, #post-cover img",
pswpModule: () => pswp,
const lightbox = new PhotoSwipeLightbox({
gallery: "body",
children: zoomTargetSelector,
initialZoomLevel: "fit",
pswpModule: () => pswpModule,
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
padding: { top: 20, bottom: 20, left: 20, right: 20 },
showHideAnimationType: "fade",
showAnimationDuration: 160,
hideAnimationDuration: 140,
secondaryZoomLevel: (zoomLevel) => Math.min(2.5, zoomLevel.max),
wheelToZoom: true,
arrowPrev: false,
arrowNext: false,
imageClickAction: 'close',
tapAction: 'close',
doubleTapAction: 'zoom',
imageClickAction: "close",
tapAction: "close",
doubleTapAction: "zoom",
})
lightbox.addFilter("domItemData", (itemData, element) => {
if (element instanceof HTMLImageElement) {
itemData.src = element.src
const fullSrc = element.dataset.pswpSrc || element.currentSrc || element.src
const width = Number(element.dataset.pswpWidth) || element.naturalWidth || element.width || window.innerWidth
const height = Number(element.dataset.pswpHeight) || element.naturalHeight || element.height || window.innerHeight
const thumbSrc = element.currentSrc || element.src
const sourceElement = element.closest(zoomTargetSelector)
const allImages = getZoomTargets()
const index = sourceElement instanceof HTMLImageElement ? allImages.indexOf(sourceElement) : -1
itemData.w = Number(element.naturalWidth || window.innerWidth)
itemData.h = Number(element.naturalHeight || window.innerHeight)
itemData.msrc = element.src
itemData.src = fullSrc
itemData.w = Number(width)
itemData.h = Number(height)
itemData.msrc = thumbSrc
if (index >= 0) {
itemData.element = sourceElement
}
}
return itemData
})
lightbox.addFilter("clickedIndex", (clickedIndex, event) => {
const target = event.target instanceof Element ? event.target.closest(zoomTargetSelector) : null
if (!(target instanceof HTMLImageElement)) {
return clickedIndex
}
const allImages = getZoomTargets()
const index = allImages.indexOf(target)
return index >= 0 ? index : clickedIndex
})
lightbox.on("uiRegister", () => {
lightbox.pswp?.ui?.registerElement({
name: "open-link",
order: 8,
isButton: true,
tagName: "a",
className: "pswp__button pswp__button--open-link",
title: "打开原图",
html: '<svg class="pswp__icn" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3Zm5 16V11h2v10H3V3h10v2H5v14h14Z"/></svg>',
onInit: (el, pswp) => {
if (!(el instanceof HTMLAnchorElement)) {
return
}
el.target = "_blank"
el.rel = "noreferrer noopener"
const syncHref = () => {
const currSlide = pswp.currSlide
const link = typeof currSlide?.data?.src === "string" ? currSlide.data.src : ""
el.href = link
el.classList.toggle("pswp__button--disabled", !link)
el.setAttribute("aria-disabled", link ? "false" : "true")
el.tabIndex = link ? 0 : -1
}
pswp.on("change", syncHref)
syncHref()
},
})
})
lightbox.init()
photoSwipeState.lightbox = lightbox
}
const setup = () => {
window.swup.hooks.on("page:view", () => {
if (lightbox) {
lightbox.destroy()
function initPhotoSwipe() {
if (photoSwipeState.lightbox || getZoomTargets().length === 0) {
return
}
createPhotoSwipe()
}
function reinitPhotoSwipe() {
destroyPhotoSwipe()
window.requestAnimationFrame(() => {
initPhotoSwipe()
})
}
if (window.swup) {
setup()
function registerPhotoSwipeHook() {
if (photoSwipeState.hookRegistered || !window.swup?.hooks) {
return
}
window.swup.hooks.on("page:view", reinitPhotoSwipe)
photoSwipeState.hookRegistered = true
}
function scheduleInitialPhotoSwipe() {
window.requestAnimationFrame(() => {
initPhotoSwipe()
})
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scheduleInitialPhotoSwipe, { once: true })
} else {
document.addEventListener("swup:enable", setup)
scheduleInitialPhotoSwipe()
}
if (!photoSwipeState.pageLoadRegistered) {
document.addEventListener("astro:page-load", scheduleInitialPhotoSwipe)
photoSwipeState.pageLoadRegistered = true
}
if (window.swup?.hooks) {
registerPhotoSwipeHook()
} else {
document.addEventListener("swup:enable", registerPhotoSwipeHook, { once: true })
}
</script>
+55 -7
View File
@@ -1,5 +1,6 @@
---
import path from "node:path";
import { render, type CollectionEntry } from "astro:content";
import License from "@components/misc/License.astro";
import Markdown from "@components/misc/Markdown.astro";
import I18nKey from "@i18n/i18nKey";
@@ -17,15 +18,18 @@ import { formatDateToYYYYMMDD } from "../../utils/date-utils";
export async function getStaticPaths() {
const blogEntries = await getSortedPosts();
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings } = await entry.render();
interface Props {
entry: CollectionEntry<"posts">;
}
const { remarkPluginFrontmatter } = await entry.render();
const { entry }: Props = Astro.props;
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
const postSlug = entry.id;
const jsonLd = {
"@context": "https://schema.org",
@@ -59,7 +63,7 @@ const customcover = entry.data.customcover;
{}
]}>
<!-- word count and reading time -->
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
<div class="flex flex-row flex-wrap text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:notes-rounded"></Icon>
@@ -74,6 +78,12 @@ const customcover = entry.data.customcover;
大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
</div>
</div>
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:visibility-outline-rounded"></Icon>
</div>
<div class="text-sm" id="post-top-page-views">加载中...</div>
</div>
</div>
<!-- title -->
@@ -82,7 +92,8 @@ const customcover = entry.data.customcover;
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
class="transition w-full block font-bold mb-3
text-3xl md:text-[2.25rem]/[2.75rem]
text-black/90 dark:text-white/90
text-[var(--primary)]
underline decoration-[3px] decoration-[var(--primary)] underline-offset-[0.45rem]
md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[0.75rem] before:left-[-1.125rem]
">
@@ -134,7 +145,7 @@ const customcover = entry.data.customcover;
<Content />
</Markdown>
{licenseConfig.enable && <License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
{licenseConfig.enable && <License title={entry.data.title} slug={entry.id} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
</div>
</div>
@@ -161,6 +172,43 @@ const customcover = entry.data.customcover;
</a>
</div>
<script define:vars={{ slug: postSlug }}>
async function fetchTopPageViews() {
const displayElement = document.getElementById('post-top-page-views');
if (!displayElement || displayElement.dataset.umamiState === 'loading' || displayElement.dataset.umamiState === 'loaded') {
return;
}
displayElement.dataset.umamiState = 'loading';
try {
const umamiStore = window['__blogUmami'];
const statsData = await umamiStore?.getStats(`post:${slug}`, (websiteId) => {
const currentTimestamp = Date.now();
return `https://umami.adclosenn.top/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent('Asia/Shanghai')}&url=%2Fposts%2F${slug}%2F&compare=false`;
});
if (!statsData) {
throw new Error('统计功能未启用');
}
const pageViews = statsData.pageviews?.value || 0;
displayElement.textContent = `浏览量 ${pageViews}`;
displayElement.dataset.umamiState = 'loaded';
} catch (error) {
console.error('Error fetching top page views:', error);
displayElement.textContent = '统计不可用';
displayElement.dataset.umamiState = 'error';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fetchTopPageViews);
} else {
fetchTopPageViews();
}
</script>
<!-- 评论区 -->
<script src="https://giscus.app/client.js"
data-repo="Ad-closeNN/blog-friends"
+2 -3
View File
@@ -1,7 +1,6 @@
import rss from '@astrojs/rss';
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';
import { getCollection } from 'astro:content';
import { siteConfig } from '@/config';
import { parse as htmlParser } from 'node-html-parser';
import { getImage } from 'astro:assets';
@@ -27,7 +26,7 @@ export async function GET(context: APIContext) {
for (const post of posts) {
// convert markdown to html string
const body = markdownParser.render(post.body);
const body = markdownParser.render(post.body ?? '');
// convert html string to DOM-like structure
const html = htmlParser.parse(body);
// hold all img tags in variable images
@@ -66,7 +65,7 @@ export async function GET(context: APIContext) {
title: post.data.title,
description: post.data.description,
pubDate: post.data.published,
link: `/posts/${post.slug}/`,
link: `/posts/${post.id}/`,
// sanitize the new html string with corrected image paths
content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+25
View File
@@ -0,0 +1,25 @@
import { visit } from "unist-util-visit";
function rewritePublicPath(url) {
if (typeof url !== "string") {
return url;
}
if (url === "/public") {
return "/";
}
if (url.startsWith("/public/")) {
return url.slice("/public".length);
}
return url;
}
export function remarkPublicImagePaths() {
return (tree) => {
visit(tree, "image", (node) => {
node.url = rewritePublicPath(node.url);
});
};
}
+27 -2
View File
@@ -1,12 +1,37 @@
.pswp__button {
@apply transition bg-black/40 hover:bg-black/50 active:bg-black/60 flex items-center justify-center mr-0 w-12 h-12 !important;
}
.pswp__button--zoom, .pswp__button--close {
.pswp__button--zoom, .pswp__button--open-link, .pswp__button--close {
@apply mt-4 rounded-xl active:scale-90 !important;
}
.pswp__button--zoom {
.pswp__button--zoom, .pswp__button--open-link {
@apply mr-2.5 !important;
}
.pswp__button--open-link {
@apply no-underline !important;
color: white !important;
}
.pswp__button--close {
@apply mr-4 !important;
}
.pswp__button--disabled {
@apply opacity-40 pointer-events-none !important;
}
.pswp__icn {
@apply fill-white !important;
}
.pswp__icn-shadow {
display: none !important;
}
.pswp__button--open-link .pswp__icn {
width: 24px !important;
height: 24px !important;
}
.pswp__button--open-link .pswp__icn path {
fill: currentColor !important;
}
+10 -6
View File
@@ -1,27 +1,31 @@
/* Page transition animations with Swup */
html.is-changing .transition-swup-fade {
@apply transition-all duration-200
transition-property: opacity, transform;
transition-duration: 180ms;
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
will-change: opacity, transform;
}
html.is-animating .transition-swup-fade {
@apply opacity-0 translate-y-4
opacity: 0;
transform: scale(0.985);
}
/* Fade-in animations for components */
@keyframes fade-in-up {
@keyframes fade-in-soft {
0% {
transform: translateY(2rem);
opacity: 0;
transform: scale(0.992);
}
100% {
transform: translateY(0);
opacity: 1;
transform: scale(1);
}
}
/* Main components */
.onload-animation {
opacity: 0;
animation: 300ms fade-in-up;
animation: 220ms fade-in-soft cubic-bezier(0.22, 1, 0.36, 1);
animation-fill-mode: forwards;
}
#navbar {
+22 -4
View File
@@ -1,4 +1,4 @@
import { type CollectionEntry, getCollection } from "astro:content";
import { render, type CollectionEntry, getCollection } from "astro:content";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
@@ -21,11 +21,11 @@ export async function getSortedPosts() {
const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) {
sorted[i].data.nextSlug = sorted[i - 1].slug;
sorted[i].data.nextSlug = sorted[i - 1].id;
sorted[i].data.nextTitle = sorted[i - 1].data.title;
}
for (let i = 0; i < sorted.length - 1; i++) {
sorted[i].data.prevSlug = sorted[i + 1].slug;
sorted[i].data.prevSlug = sorted[i + 1].id;
sorted[i].data.prevTitle = sorted[i + 1].data.title;
}
@@ -40,12 +40,30 @@ export async function getSortedPostsList(): Promise<PostForList[]> {
// delete post.body
const sortedPostsList = sortedFullPosts.map((post) => ({
slug: post.slug,
slug: post.id,
data: post.data,
}));
return sortedPostsList;
}
let totalWordsCache: number | undefined;
export async function getTotalWords(): Promise<number> {
if (totalWordsCache !== undefined) {
return totalWordsCache;
}
const posts = await getRawSortedPosts();
const renderedPosts = await Promise.all(posts.map((post) => render(post)));
const totalWords = renderedPosts.reduce((sum, { remarkPluginFrontmatter }) => {
const words = Number(remarkPluginFrontmatter?.words ?? 0);
return sum + (Number.isFinite(words) ? words : 0);
}, 0);
totalWordsCache = totalWords;
return totalWords;
}
export type Tag = {
name: string;
count: number;