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 yarn.lock
src/content/.obsidian 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 { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js"; import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
import { remarkExcerpt } from "./src/plugins/remark-excerpt.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 { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js"; import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
import rehypeExternalLinks from 'rehype-external-links'; import rehypeExternalLinks from 'rehype-external-links';
@@ -108,6 +109,7 @@ export default defineConfig({
remarkMath, remarkMath,
remarkReadingTime, remarkReadingTime,
remarkExcerpt, remarkExcerpt,
remarkPublicImagePaths,
remarkGithubAdmonitionsToDirectives, remarkGithubAdmonitionsToDirectives,
remarkDirective, remarkDirective,
remarkSectionize, remarkSectionize,
+26 -26
View File
@@ -16,32 +16,32 @@
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.8",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.4.2", "@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "7.1.0", "@astrojs/svelte": "8.0.5",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
"@expressive-code/core": "^0.41.3", "@expressive-code/core": "^0.41.7",
"@expressive-code/plugin-collapsible-sections": "^0.41.3", "@expressive-code/plugin-collapsible-sections": "^0.41.7",
"@expressive-code/plugin-line-numbers": "^0.41.3", "@expressive-code/plugin-line-numbers": "^0.41.7",
"@iconify-json/fa6-brands": "^1.2.6", "@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/fa6-regular": "^1.2.4", "@iconify-json/fa6-regular": "^1.2.4",
"@iconify-json/fa6-solid": "^1.2.4", "@iconify-json/fa6-solid": "^1.2.4",
"@iconify-json/ic": "^1.2.2", "@iconify-json/ic": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.30", "@iconify-json/material-symbols": "^1.2.67",
"@iconify/svelte": "^4.2.0", "@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.7.0", "@swup/astro": "^1.8.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.19",
"astro": "5.12.8", "astro": "6.1.8",
"astro-expressive-code": "^0.41.3", "astro-expressive-code": "^0.41.7",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"hastscript": "^9.0.1", "hastscript": "^9.0.1",
"katex": "^0.16.22", "katex": "^0.16.45",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.1",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"node-html-parser": "^7.0.1", "node-html-parser": "^7.1.0",
"overlayscrollbars": "^2.11.4", "overlayscrollbars": "^2.15.1",
"pagefind": "^1.3.0", "pagefind": "^1.5.2",
"photoswipe": "^5.4.4", "photoswipe": "^5.4.4",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
@@ -54,22 +54,22 @@
"remark-github-admonitions-to-directives": "^1.0.5", "remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0", "remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.3",
"sharp": "^0.34.3", "sharp": "^0.34.5",
"stylus": "^0.64.0", "stylus": "^0.64.0",
"svelte": "^5.37.3", "svelte": "^5.55.4",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.19",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.4", "@astrojs/ts-plugin": "^1.10.7",
"@biomejs/biome": "2.1.3", "@biomejs/biome": "2.1.3",
"@rollup/plugin-yaml": "^4.1.2", "@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.1",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"postcss-nesting": "^13.0.2" "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: { data: {
title: string; title: string;
tags: string[]; tags: string[];
category?: string; category?: string | null;
published: Date; published: Date;
}; };
} }
+6 -5
View File
@@ -7,6 +7,7 @@ import { i18n } from "../i18n/translation";
import { getDir } from "../utils/url-utils"; import { getDir } from "../utils/url-utils";
import ImageWrapper from "./misc/ImageWrapper.astro"; import ImageWrapper from "./misc/ImageWrapper.astro";
import PostMetadata from "./PostMeta.astro"; import PostMetadata from "./PostMeta.astro";
import { render } from 'astro:content';
interface Props { interface Props {
class?: string; class?: string;
@@ -20,7 +21,7 @@ interface Props {
image: string; image: string;
description: string; description: string;
draft: boolean; draft: boolean;
style: string; style?: string;
} }
const { const {
entry, entry,
@@ -32,7 +33,7 @@ const {
category, category,
image, image,
description, description,
style, style = "",
} = Astro.props; } = Astro.props;
const className = Astro.props.class; const className = Astro.props.class;
@@ -40,7 +41,7 @@ const hasCover = image !== undefined && image !== null && image !== "";
const coverWidth = "28%"; 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={["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}]}> <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>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
<div>|</div> <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>
</div> </div>
@@ -110,7 +111,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/PostCard.astro --> <!-- 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) { async function fetchPostCardViews(slug) {
const displayElement = document.getElementById(`page-views-${slug}`); const displayElement = document.getElementById(`page-views-${slug}`);
+1 -1
View File
@@ -17,7 +17,7 @@ const interval = 50;
category={entry.data.category} category={entry.data.category}
published={entry.data.published} published={entry.data.published}
updated={entry.data.updated} updated={entry.data.updated}
url={getPostUrlBySlug(entry.slug)} url={getPostUrlBySlug(entry.id)}
image={entry.data.image} image={entry.data.image}
description={entry.data.description} description={entry.data.description}
draft={entry.data.draft} draft={entry.data.draft}
+5 -2
View File
@@ -46,9 +46,12 @@ if (isLocal) {
const imageClass = "w-full h-full object-cover"; const imageClass = "w-full h-full object-cover";
const imageStyle = `object-position: ${position}`; 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 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> <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 && <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={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>} {!isLocal && <img src={originalSrc} alt={alt || ""} class={imageClass} style={imageStyle} data-pswp-src={originalSrc} data-original-src={originalSrc}/>}
</div> </div>
+53 -31
View File
@@ -3,6 +3,7 @@ import { Icon } from "astro-icon/components";
import { profileConfig } from "../../config"; import { profileConfig } from "../../config";
import { url } from "../../utils/url-utils"; import { url } from "../../utils/url-utils";
import ImageWrapper from "../misc/ImageWrapper.astro"; import ImageWrapper from "../misc/ImageWrapper.astro";
import TotalWords from "./TotalWords.astro";
const config = profileConfig; const config = profileConfig;
--- ---
@@ -45,6 +46,8 @@ const config = profileConfig;
</a>} </a>}
</div> </div>
<TotalWords />
<!-- 全站访问量统计 https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/widget/Profile.astro --> <!-- 全站访问量统计 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="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"> <div class="flex items-center justify-center gap-1">
@@ -74,18 +77,37 @@ const config = profileConfig;
fetch("https://v1.hitokoto.cn") fetch("https://v1.hitokoto.cn")
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// API 返回的字段是 data.text const hitokotoElement = document.getElementById("hitokoto");
document.getElementById("hitokoto").innerText = data.hitokoto; if (hitokotoElement) {
hitokotoElement.innerText = data.hitokoto;
}
}) })
.catch(error => { .catch(error => {
console.error("获取一言失败:", error); console.error("获取一言失败:", error);
//document.getElementById("hitokoto").innerText = "获取失败,请稍后再试。"; const hitokotoElement = document.getElementById("hitokoto");
//失败后用内置的,成功了用别人的。 if (hitokotoElement) {
document.getElementById("hitokoto").innerText = "再热情的心也经不起冷漠,再爱你的人也经不起冷落。"; hitokotoElement.innerText = "再热情的心也经不起冷漠,再爱你的人也经不起冷落。";
}
}); });
</script> </script>
<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() { async function loadSiteStats() {
const statsElement = document.getElementById('site-stats'); const statsElement = document.getElementById('site-stats');
@@ -96,8 +118,8 @@ fetch("https://v1.hitokoto.cn")
statsElement.dataset.umamiState = 'loading'; statsElement.dataset.umamiState = 'loading';
try { try {
const umamiStore = window['__blogUmami']; const umamiStore = window.__blogUmami;
const statsData = await umamiStore?.getStats('site:all', (websiteId) => { const statsData = await umamiStore?.getStats('site:all', (websiteId: string) => {
const currentTimestamp = Date.now(); 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`; 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> </script>
<!-- 获取 Commit 信息 via API --> <!-- 获取 Commit 信息 via API -->
<script> <script>
type GithubCommit = {
sha: string;
html_url: string;
commit: {
message: string;
committer: {
date: string;
};
};
};
async function loadCommitStats() { async function loadCommitStats() {
try { try {
const statsElement = document.getElementById('github-commit'); // 查找 id const statsElement = document.getElementById('github-commit');
const link = document.getElementById('github-commit-link'); // 查找 id 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`); const githubResponse = await fetch(`https://api.github.com/repos/Ad-closeNN/blog-fuwari/commits?per_page=1`);
if (!githubResponse.ok) { if (!githubResponse.ok) {
throw new Error('获取信息失败'); throw new Error('获取信息失败');
} }
let Data = await githubResponse.json(); const data = (await githubResponse.json()) as GithubCommit[];
Data = Data[0]; const latestCommit = data[0];
// 第二步:获取 Commit 数据
const latestCommit = Data;
const commitData = { if (!latestCommit) {
hash: latestCommit.sha.slice(0, 7), throw new Error('未获取到提交信息');
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 (statsElement) {
statsElement.textContent = `当前提交:${latestCommit.sha.slice(0, 7)}`;
}
if (link){ if (link instanceof HTMLAnchorElement) {
// const gurl = "https://github.com/Ad-closeNN/blog-fuwari/commit/"+Data.sha; const infoUrl = "/info/";
const gurl = "/info/"; link.href = infoUrl;
link.href = gurl; link.title = `(${latestCommit.commit.committer.date}) ${latestCommit.commit.message}`;
link.title = "("+Data.commit.committer.date + ")" + " " + Data.commit.message;
} }
} catch (error) { } catch (error) {
console.error('获取 Commit 信息失败:', 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"} ::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) 一位住在 [中华人民共和国广西壮族自治区](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) 电子邮箱:[admin@adclosenn.top](mailto:admin@adclosenn.top)
Discordhttps://discord.com/users/1068060784300658688 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 { declare global {
interface Window { interface Window {
// type from '@swup/astro' is incorrect swup?: SwupLike;
swup: AstroIntegration; __blogPhotoSwipe?: BlogPhotoSwipeState;
dataLayer?: unknown[];
pagefind: { pagefind: {
search: (query: string) => Promise<{ search: (query: string) => Promise<{
results: Array<{ results: Array<{
@@ -14,6 +37,9 @@ declare global {
} }
} }
declare function gtag(...args: unknown[]): void;
declare let dataLayer: unknown[];
interface SearchResult { interface SearchResult {
url: string; url: string;
meta: { meta: {
+134 -24
View File
@@ -482,8 +482,8 @@ function showBanner() {
} }
// 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发 // 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发
const animatedElements = document.querySelectorAll('.onload-animation'); const animatedElements = document.querySelectorAll<HTMLElement>('.onload-animation');
animatedElements.forEach(el => { animatedElements.forEach((el) => {
el.style.animation = 'none'; el.style.animation = 'none';
el.style.opacity = '1'; el.style.opacity = '1';
}); });
@@ -634,53 +634,163 @@ window.onresize = () => {
<script> <script>
import PhotoSwipeLightbox from "photoswipe/lightbox" import PhotoSwipeLightbox from "photoswipe/lightbox"
import "photoswipe/style.css" import "photoswipe/style.css"
import "../styles/photoswipe.css"
let lightbox: PhotoSwipeLightbox const zoomTargetSelector = ".custom-md img, #post-cover img"
let pswp = import("photoswipe") 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() { function createPhotoSwipe() {
lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
gallery: ".custom-md img, #post-cover img", gallery: "body",
pswpModule: () => pswp, 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>', 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>', 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 }, 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, wheelToZoom: true,
arrowPrev: false, arrowPrev: false,
arrowNext: false, arrowNext: false,
imageClickAction: 'close', imageClickAction: "close",
tapAction: 'close', tapAction: "close",
doubleTapAction: 'zoom', doubleTapAction: "zoom",
}) })
lightbox.addFilter("domItemData", (itemData, element) => { lightbox.addFilter("domItemData", (itemData, element) => {
if (element instanceof HTMLImageElement) { 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.src = fullSrc
itemData.h = Number(element.naturalHeight || window.innerHeight) itemData.w = Number(width)
itemData.h = Number(height)
itemData.msrc = element.src itemData.msrc = thumbSrc
if (index >= 0) {
itemData.element = sourceElement
}
} }
return itemData 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() lightbox.init()
photoSwipeState.lightbox = lightbox
} }
const setup = () => { function initPhotoSwipe() {
window.swup.hooks.on("page:view", () => { if (photoSwipeState.lightbox || getZoomTargets().length === 0) {
if (lightbox) { return
lightbox.destroy() }
} createPhotoSwipe()
createPhotoSwipe() }
function reinitPhotoSwipe() {
destroyPhotoSwipe()
window.requestAnimationFrame(() => {
initPhotoSwipe()
}) })
} }
if (window.swup) { function registerPhotoSwipeHook() {
setup() 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 { } 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> </script>
+55 -7
View File
@@ -1,5 +1,6 @@
--- ---
import path from "node:path"; import path from "node:path";
import { render, type CollectionEntry } from "astro:content";
import License from "@components/misc/License.astro"; import License from "@components/misc/License.astro";
import Markdown from "@components/misc/Markdown.astro"; import Markdown from "@components/misc/Markdown.astro";
import I18nKey from "@i18n/i18nKey"; import I18nKey from "@i18n/i18nKey";
@@ -17,15 +18,18 @@ import { formatDateToYYYYMMDD } from "../../utils/date-utils";
export async function getStaticPaths() { export async function getStaticPaths() {
const blogEntries = await getSortedPosts(); const blogEntries = await getSortedPosts();
return blogEntries.map((entry) => ({ return blogEntries.map((entry) => ({
params: { slug: entry.slug }, params: { slug: entry.id },
props: { entry }, props: { entry },
})); }));
} }
const { entry } = Astro.props; interface Props {
const { Content, headings } = await entry.render(); 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 = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -59,7 +63,7 @@ const customcover = entry.data.customcover;
{} {}
]}> ]}>
<!-- word count and reading time --> <!-- 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="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"> <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> <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)} 大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
</div> </div>
</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> </div>
<!-- title --> <!-- title -->
@@ -82,7 +92,8 @@ const customcover = entry.data.customcover;
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title" data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
class="transition w-full block font-bold mb-3 class="transition w-full block font-bold mb-3
text-3xl md:text-[2.25rem]/[2.75rem] 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)] md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[0.75rem] before:left-[-1.125rem] before:absolute before:top-[0.75rem] before:left-[-1.125rem]
"> ">
@@ -134,7 +145,7 @@ const customcover = entry.data.customcover;
<Content /> <Content />
</Markdown> </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>
</div> </div>
@@ -161,6 +172,43 @@ const customcover = entry.data.customcover;
</a> </a>
</div> </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" <script src="https://giscus.app/client.js"
data-repo="Ad-closeNN/blog-friends" data-repo="Ad-closeNN/blog-friends"
+2 -3
View File
@@ -1,7 +1,6 @@
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import { getCollection } from 'astro:content';
import { siteConfig } from '@/config'; import { siteConfig } from '@/config';
import { parse as htmlParser } from 'node-html-parser'; import { parse as htmlParser } from 'node-html-parser';
import { getImage } from 'astro:assets'; import { getImage } from 'astro:assets';
@@ -27,7 +26,7 @@ export async function GET(context: APIContext) {
for (const post of posts) { for (const post of posts) {
// convert markdown to html string // convert markdown to html string
const body = markdownParser.render(post.body); const body = markdownParser.render(post.body ?? '');
// convert html string to DOM-like structure // convert html string to DOM-like structure
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
// hold all img tags in variable images // hold all img tags in variable images
@@ -66,7 +65,7 @@ export async function GET(context: APIContext) {
title: post.data.title, title: post.data.title,
description: post.data.description, description: post.data.description,
pubDate: post.data.published, pubDate: post.data.published,
link: `/posts/${post.slug}/`, link: `/posts/${post.id}/`,
// sanitize the new html string with corrected image paths // sanitize the new html string with corrected image paths
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), 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 { .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; @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; @apply mt-4 rounded-xl active:scale-90 !important;
} }
.pswp__button--zoom { .pswp__button--zoom, .pswp__button--open-link {
@apply mr-2.5 !important; @apply mr-2.5 !important;
} }
.pswp__button--open-link {
@apply no-underline !important;
color: white !important;
}
.pswp__button--close { .pswp__button--close {
@apply mr-4 !important; @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 */ /* Page transition animations with Swup */
html.is-changing .transition-swup-fade { 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 { html.is-animating .transition-swup-fade {
@apply opacity-0 translate-y-4 opacity: 0;
transform: scale(0.985);
} }
/* Fade-in animations for components */ /* Fade-in animations for components */
@keyframes fade-in-up { @keyframes fade-in-soft {
0% { 0% {
transform: translateY(2rem);
opacity: 0; opacity: 0;
transform: scale(0.992);
} }
100% { 100% {
transform: translateY(0);
opacity: 1; opacity: 1;
transform: scale(1);
} }
} }
/* Main components */ /* Main components */
.onload-animation { .onload-animation {
opacity: 0; opacity: 0;
animation: 300ms fade-in-up; animation: 220ms fade-in-soft cubic-bezier(0.22, 1, 0.36, 1);
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
#navbar { #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 I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation"; import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts"; import { getCategoryUrl } from "@utils/url-utils.ts";
@@ -21,11 +21,11 @@ export async function getSortedPosts() {
const sorted = await getRawSortedPosts(); const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) { 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; sorted[i].data.nextTitle = sorted[i - 1].data.title;
} }
for (let i = 0; i < sorted.length - 1; i++) { 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; sorted[i].data.prevTitle = sorted[i + 1].data.title;
} }
@@ -40,12 +40,30 @@ export async function getSortedPostsList(): Promise<PostForList[]> {
// delete post.body // delete post.body
const sortedPostsList = sortedFullPosts.map((post) => ({ const sortedPostsList = sortedFullPosts.map((post) => ({
slug: post.slug, slug: post.id,
data: post.data, data: post.data,
})); }));
return sortedPostsList; 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 = { export type Tag = {
name: string; name: string;
count: number; count: number;