diff --git a/.gitignore b/.gitignore
index 4ade5e6..5bf8f81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,4 +35,5 @@ yarn.lock
# .obsidian
.cache
-build.log
\ No newline at end of file
+build.log
+.traces
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 299a617..4e2cd1b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -99,3 +99,4 @@ src/
- Biome 不处理 CSS 文件(在 `biome.json` 中排除)
- Svelte 组件使用 Svelte 5 语法
- 主题色基于 CSS 变量 `--hue` 动态计算
+- 不要执行构建,除非特别要求
\ No newline at end of file
diff --git a/src/components/ConfigCarrier.astro b/src/components/ConfigCarrier.astro
index 71a7621..a60fb53 100644
--- a/src/components/ConfigCarrier.astro
+++ b/src/components/ConfigCarrier.astro
@@ -1,7 +1,6 @@
---
import { siteConfig } from "../config";
-
---
diff --git a/src/components/misc/ImageWrapper.astro b/src/components/misc/ImageWrapper.astro
index c4e0fee..8fb4e65 100644
--- a/src/components/misc/ImageWrapper.astro
+++ b/src/components/misc/ImageWrapper.astro
@@ -15,7 +15,15 @@ interface Props {
import { Image } from "astro:assets";
import { url } from "../../utils/url-utils";
-const { id, src, alt, position = "center", basePath = "/", loader = false, blur = false } = Astro.props;
+const {
+ id,
+ src,
+ alt,
+ position = "center",
+ basePath = "/",
+ loader = false,
+ blur = false,
+} = Astro.props;
const className = Astro.props.class;
const isLocal = !(
@@ -48,8 +56,14 @@ if (isLocal) {
const imageClass = "w-full h-full object-cover";
const imageStyle = `object-position: ${position}`;
-const normalizedPublicSrc = src === "/public" ? "/" : src.startsWith("/public/") ? src.slice("/public".length) : src;
-const originalSrc = isLocal && img ? img.src : isPublic ? url(normalizedPublicSrc) : src;
+const normalizedPublicSrc =
+ src === "/public"
+ ? "/"
+ : src.startsWith("/public/")
+ ? src.slice("/public".length)
+ : src;
+const originalSrc =
+ isLocal && img ? img.src : isPublic ? url(normalizedPublicSrc) : src;
const originalWidth = isLocal && img ? img.width : undefined;
const originalHeight = isLocal && img ? img.height : undefined;
---
diff --git a/src/components/widget/DnsFeedbackBanner.svelte b/src/components/widget/DnsFeedbackBanner.svelte
new file mode 100644
index 0000000..4409c31
--- /dev/null
+++ b/src/components/widget/DnsFeedbackBanner.svelte
@@ -0,0 +1,291 @@
+
+
+{#if !dismissed}
+
+
+
+
本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。
+
+ {#if showFeedback}
+
+ {/if}
+
+
+
+
+
+
+
+{#if isOpen}
+
+
+
+
+
+
+
+
+
+
+ DNS 污染反馈
+
+
+
+
+
+
+ {#if feedbackState === "animating"}
+
+ {:else}
+
+ 本站是否能够在你所在地区被正常访问
+
+
+ {#if isError}
+
提交失败,请稍后重试
+ {/if}
+
+
+
+
+
+ {/if}
+
+
+
+{/if}
+{/if}
+
+
diff --git a/src/config.ts b/src/config.ts
index ec537fb..f1402d5 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,6 @@
import type {
ExpressiveCodeConfig,
+ FeedbackConfig,
LicenseConfig,
NavBarConfig,
ProfileConfig,
@@ -43,8 +44,8 @@ export const siteConfig: SiteConfig = {
},
favicon: [
{
- src: '/assets/avatar.jpg',
- }
+ src: "/assets/avatar.jpg",
+ },
// Leave this array empty to use the default favicon
// {
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
@@ -126,9 +127,15 @@ export const expressiveCodeConfig: ExpressiveCodeConfig = {
theme: "github-dark",
};
+export const feedbackConfig: FeedbackConfig = {
+ enable: true,
+ apiEndpoint: "https://blog-feedback.adclosenn.top",
+ debug: false,
+};
+
export const umamiConfig: UmamiConfig = {
enable: true,
baseUrl: "https://umami.adclosenn.top",
shareId: "jME4HFb9JmfJM5zs",
timezone: "Asia/Shanghai",
-};
\ No newline at end of file
+};
diff --git a/src/content.config.ts b/src/content.config.ts
index 9f50203..1514319 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -1,41 +1,41 @@
import { defineCollection } from "astro:content";
-import { z } from "astro/zod";
import { glob } from "astro/loaders";
+import { z } from "astro/zod";
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(""),
- pinned: z.boolean().optional().default(false),
- outdated: z.boolean().optional().default(false),
- lang: z.string().optional().default(""),
- prevTitle: z.string().default(""),
- prevSlug: z.string().default(""),
- nextTitle: z.string().default(""),
- nextSlug: z.string().default(""),
- }),
+ 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(""),
+ pinned: z.boolean().optional().default(false),
+ outdated: z.boolean().optional().default(false),
+ 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),
- }),
+ 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,
+ posts: postsCollection,
+ spec: specCollection,
};
diff --git a/src/i18n/i18nKey.ts b/src/i18n/i18nKey.ts
index f796b17..d17cdb0 100644
--- a/src/i18n/i18nKey.ts
+++ b/src/i18n/i18nKey.ts
@@ -32,6 +32,8 @@ enum I18nKey {
author = "author",
publishedAt = "publishedAt",
license = "license",
+
+ dnsFeedbackError = "dnsFeedbackError",
}
export default I18nKey;
diff --git a/src/i18n/languages/zh_CN.ts b/src/i18n/languages/zh_CN.ts
index d1b8cf1..820ae3f 100644
--- a/src/i18n/languages/zh_CN.ts
+++ b/src/i18n/languages/zh_CN.ts
@@ -35,4 +35,6 @@ export const zh_CN: Translation = {
[Key.author]: "作者",
[Key.publishedAt]: "发布于",
[Key.license]: "许可协议",
+
+ [Key.dnsFeedbackError]: "提交失败",
};
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 3e0782c..1b03e7c 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -1,6 +1,6 @@
---
import ConfigCarrier from "@components/ConfigCarrier.astro";
-import { Icon } from "astro-icon/components";
+import DnsFeedbackBanner from "@components/widget/DnsFeedbackBanner.svelte";
import { profileConfig, siteConfig, umamiConfig } from "@/config";
import {
AUTO_MODE,
@@ -274,29 +274,7 @@ const bannerOffset =
data-overlayscrollbars-initialize
>
-
-
-
-
本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。
-
-
-
-
+
diff --git a/src/pages/info.astro b/src/pages/info.astro
index 481930e..250c50d 100644
--- a/src/pages/info.astro
+++ b/src/pages/info.astro
@@ -1,11 +1,12 @@
---
import { getEntry, render } from "astro:content";
+import { addIssueToContext } from "astro:schema";
import Markdown from "@components/misc/Markdown.astro";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import MainGridLayout from "../layouts/MainGridLayout.astro";
-import { addIssueToContext } from "astro:schema";
+
const aboutPost = await getEntry("spec", "info");
if (!aboutPost) {
diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro
index 844ca56..dd85ae0 100644
--- a/src/pages/posts/[...slug].astro
+++ b/src/pages/posts/[...slug].astro
@@ -62,16 +62,25 @@ function getPostSourcePath(sourceKey: string | undefined) {
return sourceKey?.replace("../../content/posts/", "src/content/posts/");
}
-function getGitHubPostSourceUrl(repo: string | undefined, sourcePath: string | undefined) {
+function getGitHubPostSourceUrl(
+ repo: string | undefined,
+ sourcePath: string | undefined,
+) {
if (!repo || !sourcePath) return "";
const repoUrl = repo.includes("://") ? repo : `https://github.com/${repo}`;
const normalizedRepo = repoUrl.replace(/\.git$/, "").replace(/\/+$/, "");
- const encodedSourcePath = sourcePath.split("/").map(encodeURIComponent).join("/");
+ const encodedSourcePath = sourcePath
+ .split("/")
+ .map(encodeURIComponent)
+ .join("/");
return `${normalizedRepo}/blob/main/${encodedSourcePath}?plain=1`;
}
-function getGitHubRawSourceUrl(repo: string | undefined, sourcePath: string | undefined) {
+function getGitHubRawSourceUrl(
+ repo: string | undefined,
+ sourcePath: string | undefined,
+) {
if (!repo || !sourcePath) return "";
const normalizedRepo = repo.replace(/\.git$/, "").replace(/\/+$/, "");
@@ -81,11 +90,18 @@ function getGitHubRawSourceUrl(repo: string | undefined, sourcePath: string | un
const [owner, repository] = repoPath.split("/");
if (!owner || !repository) return "";
- const encodedSourcePath = sourcePath.split("/").map(encodeURIComponent).join("/");
+ const encodedSourcePath = sourcePath
+ .split("/")
+ .map(encodeURIComponent)
+ .join("/");
return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/refs/heads/main/${encodedSourcePath}`;
}
-function getGiteaRawSourceUrl(repo: string | undefined, sourcePath: string | undefined, giteaHost: string) {
+function getGiteaRawSourceUrl(
+ repo: string | undefined,
+ sourcePath: string | undefined,
+ giteaHost: string,
+) {
if (!repo || !sourcePath) return "";
const normalizedRepo = repo.replace(/\.git$/, "").replace(/\/+$/, "");
@@ -95,7 +111,10 @@ function getGiteaRawSourceUrl(repo: string | undefined, sourcePath: string | und
const [owner, repository] = repoPath.split("/");
if (!owner || !repository) return "";
- const encodedSourcePath = sourcePath.split("/").map(encodeURIComponent).join("/");
+ const encodedSourcePath = sourcePath
+ .split("/")
+ .map(encodeURIComponent)
+ .join("/");
return `https://${giteaHost}/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/src/branch/main/${encodedSourcePath}?display=source`;
}
@@ -103,7 +122,10 @@ function getCnbsSourceUrl(baseUrl: string, sourcePath: string | undefined) {
if (!sourcePath) return "";
const normalizedUrl = baseUrl.replace(/\.git$/, "").replace(/\/+$/, "");
- const encodedSourcePath = sourcePath.split("/").map(encodeURIComponent).join("/");
+ const encodedSourcePath = sourcePath
+ .split("/")
+ .map(encodeURIComponent)
+ .join("/");
return `${normalizedUrl}/-/blob/main/${encodedSourcePath}`;
}
@@ -160,10 +182,23 @@ function getAiSummaryMeta(entryId: string) {
const aiSummaryMeta = getAiSummaryMeta(entry.id);
const postSourceKey = getPostSourceKey(entry.id);
const postSourcePath = getPostSourcePath(postSourceKey);
-const postSourceUrl = getGitHubPostSourceUrl(siteConfig.githubRepo, postSourcePath);
-const postRawSourceUrl = getGitHubRawSourceUrl(siteConfig.githubRepo, postSourcePath);
-const postGiteaRawSourceUrl = getGiteaRawSourceUrl(siteConfig.githubRepo, postSourcePath, "git.adclosenn.top");
-const postCnbsSourceUrl = getCnbsSourceUrl("https://cnb.cool/CLN-Grated/blog-fuwari", postSourcePath);
+const postSourceUrl = getGitHubPostSourceUrl(
+ siteConfig.githubRepo,
+ postSourcePath,
+);
+const postRawSourceUrl = getGitHubRawSourceUrl(
+ siteConfig.githubRepo,
+ postSourcePath,
+);
+const postGiteaRawSourceUrl = getGiteaRawSourceUrl(
+ siteConfig.githubRepo,
+ postSourcePath,
+ "git.adclosenn.top",
+);
+const postCnbsSourceUrl = getCnbsSourceUrl(
+ "https://cnb.cool/CLN-Grated/blog-fuwari",
+ postSourcePath,
+);
const aiPrompt = `Read from ${postRawSourceUrl} so I can ask questions about it.`;
const encodedAiPrompt = encodeURIComponent(aiPrompt);
const copyPageAiLinks = [
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts
index 66de40b..78bb6f5 100644
--- a/src/pages/rss.xml.ts
+++ b/src/pages/rss.xml.ts
@@ -1,22 +1,22 @@
-import rss from '@astrojs/rss';
-import sanitizeHtml from 'sanitize-html';
-import MarkdownIt from 'markdown-it';
-import { siteConfig } from '@/config';
-import { parse as htmlParser } from 'node-html-parser';
-import { getImage } from 'astro:assets';
-import type { APIContext, ImageMetadata } from 'astro';
-import type { RSSFeedItem } from '@astrojs/rss';
-import { getSortedPosts } from '@/utils/content-utils';
+import { getImage } from "astro:assets";
+import type { RSSFeedItem } from "@astrojs/rss";
+import rss from "@astrojs/rss";
+import type { APIContext, ImageMetadata } from "astro";
+import MarkdownIt from "markdown-it";
+import { parse as htmlParser } from "node-html-parser";
+import sanitizeHtml from "sanitize-html";
+import { siteConfig } from "@/config";
+import { getSortedPosts } from "@/utils/content-utils";
const markdownParser = new MarkdownIt();
function rewritePublicPath(url: string) {
- if (url === '/public') {
- return '/';
+ if (url === "/public") {
+ return "/";
}
- if (url.startsWith('/public/')) {
- return url.slice('/public'.length);
+ if (url.startsWith("/public/")) {
+ return url.slice("/public".length);
}
return url;
@@ -24,12 +24,12 @@ function rewritePublicPath(url: string) {
// get dynamic import of images as a map collection
const imagesGlob = import.meta.glob<{ default: ImageMetadata }>(
- '/src/content/**/*.{jpeg,jpg,png,gif,webp}', // include posts and assets
+ "/src/content/**/*.{jpeg,jpg,png,gif,webp}", // include posts and assets
);
export async function GET(context: APIContext) {
if (!context.site) {
- throw Error('site not set');
+ throw Error("site not set");
}
// Use the same ordering as site listing (pinned first, then by published desc)
@@ -38,39 +38,41 @@ 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
- const images = html.querySelectorAll('img');
+ const images = html.querySelectorAll("img");
for (const img of images) {
- const srcAttr = img.getAttribute('src');
+ const srcAttr = img.getAttribute("src");
if (!srcAttr) continue;
const src = rewritePublicPath(srcAttr);
// Handle content-relative images and convert them to built _astro paths
- if (src.startsWith('./') || src.startsWith('../')) {
+ if (src.startsWith("./") || src.startsWith("../")) {
let importPath: string | null = null;
- if (src.startsWith('./')) {
+ if (src.startsWith("./")) {
// Path relative to the post file directory
const prefixRemoved = src.slice(2);
importPath = `/src/content/posts/${prefixRemoved}`;
} else {
// Path like /public/pic/xxx -> relative to /src/content/
- const cleaned = src.replace(/^\.\.\//, '');
+ const cleaned = src.replace(/^\.\.\//, "");
importPath = `/src/content/${cleaned}`;
}
- const imageMod = await imagesGlob[importPath]?.()?.then((res) => res.default);
+ const imageMod = await imagesGlob[importPath]?.()?.then(
+ (res) => res.default,
+ );
if (imageMod) {
const optimizedImg = await getImage({ src: imageMod });
- img.setAttribute('src', new URL(optimizedImg.src, context.site).href);
+ img.setAttribute("src", new URL(optimizedImg.src, context.site).href);
}
- } else if (src.startsWith('/')) {
- img.setAttribute('src', new URL(src, context.site).href);
+ } else if (src.startsWith("/")) {
+ img.setAttribute("src", new URL(src, context.site).href);
}
}
@@ -81,14 +83,14 @@ export async function GET(context: APIContext) {
link: `/posts/${post.id}/`,
// sanitize the new html string with corrected image paths
content: sanitizeHtml(html.toString(), {
- allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
}),
});
}
return rss({
title: siteConfig.title,
- description: siteConfig.subtitle || 'No description',
+ description: siteConfig.subtitle || "No description",
site: context.site,
items: feed,
customData: `
${siteConfig.lang}25050403786655846483370505718413312`,
diff --git a/src/types/config.ts b/src/types/config.ts
index f8aa0ba..fa5f853 100644
--- a/src/types/config.ts
+++ b/src/types/config.ts
@@ -21,7 +21,7 @@ export type SiteConfig = {
hue: number;
fixed: boolean;
};
-
+
background: {
enable: boolean;
src: string;
@@ -31,7 +31,7 @@ export type SiteConfig = {
attachment?: "fixed" | "scroll" | "local";
opacity?: number;
};
-
+
banner: {
enable: boolean;
src: string;
@@ -89,9 +89,7 @@ export type LicenseConfig = {
url: string;
};
-export type LIGHT_DARK_MODE =
- | typeof LIGHT_MODE
- | typeof DARK_MODE
+export type LIGHT_DARK_MODE = typeof LIGHT_MODE | typeof DARK_MODE;
export type BlogPostData = {
body: string;
@@ -124,4 +122,10 @@ export type UmamiConfig = {
baseUrl: string;
shareId: string;
timezone: string;
-};
\ No newline at end of file
+};
+
+export type FeedbackConfig = {
+ enable: boolean;
+ apiEndpoint: string;
+ debug?: boolean;
+};
diff --git a/src/utils/content-utils.ts b/src/utils/content-utils.ts
index bd09f79..c7e1ba2 100644
--- a/src/utils/content-utils.ts
+++ b/src/utils/content-utils.ts
@@ -1,4 +1,4 @@
-import { render, type CollectionEntry, getCollection } from "astro:content";
+import { type CollectionEntry, getCollection, render } from "astro:content";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
@@ -60,10 +60,13 @@ export async function getTotalWords(): Promise
{
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);
+ const totalWords = renderedPosts.reduce(
+ (sum, { remarkPluginFrontmatter }) => {
+ const words = Number(remarkPluginFrontmatter?.words ?? 0);
+ return sum + (Number.isFinite(words) ? words : 0);
+ },
+ 0,
+ );
totalWordsCache = totalWords;
return totalWords;