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:
@@ -27,3 +27,9 @@ bun.lockb
|
|||||||
yarn.lock
|
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 { 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
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2851
-3355
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"}
|
::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)
|
||||||
Discord:https://discord.com/users/1068060784300658688
|
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 {
|
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
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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']),
|
||||||
|
|||||||
@@ -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 {
|
.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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user