mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 01:20:06 -04:00
Merge pull request #3 from Ad-closeNN/feat/update-deps
feat: update deps
This commit is contained in:
+7
-1
@@ -26,4 +26,10 @@ package-lock.json
|
||||
bun.lockb
|
||||
yarn.lock
|
||||
|
||||
src/content/.obsidian
|
||||
src/content/.obsidian
|
||||
|
||||
.playwright-mcp
|
||||
.serena
|
||||
.claude
|
||||
|
||||
.obsidian
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+2851
-3355
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ interface Post {
|
||||
data: {
|
||||
title: string;
|
||||
tags: string[];
|
||||
category?: string;
|
||||
category?: string | null;
|
||||
published: Date;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,46 +147,46 @@ 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
|
||||
|
||||
// 第一步:调用 API
|
||||
const statsElement = document.getElementById('github-commit');
|
||||
const link = document.getElementById('github-commit-link');
|
||||
|
||||
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 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)}`;
|
||||
}
|
||||
|
||||
const data = (await githubResponse.json()) as GithubCommit[];
|
||||
const latestCommit = data[0];
|
||||
|
||||
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 (!latestCommit) {
|
||||
throw new Error('未获取到提交信息');
|
||||
}
|
||||
|
||||
if (statsElement) {
|
||||
statsElement.textContent = `当前提交:${latestCommit.sha.slice(0, 7)}`;
|
||||
}
|
||||
|
||||
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);
|
||||
console.error('获取 Commit 信息失败:', error);
|
||||
const statsElement = document.getElementById('github-commit');
|
||||
if (statsElement) {
|
||||
statsElement.textContent = '提交信息不可用';
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)
|
||||
Discord:https://discord.com/users/1068060784300658688
|
||||
BlueSky:https://bsky.app/profile/adclosenn.top
|
||||
X:https://x.com/Ad_closeNN
|
||||
|
||||
# 关于本站
|
||||
## 字体
|
||||
|
||||
Vendored
+29
-3
@@ -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: {
|
||||
|
||||
+134
-24
@@ -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()
|
||||
}
|
||||
createPhotoSwipe()
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user