chore: format & feedback beautification

This commit is contained in:
Ad-closeNN
2026-05-23 21:18:44 +08:00
parent f283dc2d40
commit 256d2ca844
15 changed files with 452 additions and 112 deletions
+2 -1
View File
@@ -35,4 +35,5 @@ yarn.lock
# .obsidian
.cache
build.log
build.log
.traces
+1
View File
@@ -99,3 +99,4 @@ src/
- Biome 不处理 CSS 文件(在 `biome.json` 中排除)
- Svelte 组件使用 Svelte 5 语法
- 主题色基于 CSS 变量 `--hue` 动态计算
- 不要执行构建,除非特别要求
-1
View File
@@ -1,7 +1,6 @@
---
import { siteConfig } from "../config";
---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}>
+17 -3
View File
@@ -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;
---
@@ -0,0 +1,291 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import { onMount } from "svelte";
import { cubicOut } from "svelte/easing";
import { fade, fly } from "svelte/transition";
import { feedbackConfig } from "@/config";
let dismissed = $state(true);
let feedbackDone = $state(false);
let isOpen = $state(false);
let feedbackState = $state<"idle" | "submitting" | "animating" | "error">(
"idle",
);
let lastChoice = $state<"yes" | "no" | null>(null);
let currentUrl = $state("");
let modalRef: HTMLDivElement | undefined = $state();
let showFeedback = $derived(feedbackConfig.enable && !feedbackDone);
let canSubmit = $derived(feedbackState === "idle");
let isError = $derived(feedbackState === "error");
onMount(() => {
if (!localStorage.getItem("dns-warning-dismissed")) {
dismissed = false;
}
if (localStorage.getItem("dns-feedback-submitted")) {
feedbackDone = true;
}
});
$effect(() => {
if (isOpen) {
document.documentElement.style.overflow = "hidden";
const firstBtn = modalRef?.querySelector<HTMLButtonElement>("button");
firstBtn?.focus();
} else {
document.documentElement.style.overflow = "";
}
return () => {
document.documentElement.style.overflow = "";
};
});
function dismissBar() {
dismissed = true;
localStorage.setItem("dns-warning-dismissed", "1");
}
function openModal() {
currentUrl = window.location.href;
isOpen = true;
}
function closeModal() {
isOpen = false;
if (feedbackState === "error") {
feedbackState = "idle";
lastChoice = null;
}
}
function handleOverlayClick(e: MouseEvent) {
if (e.target === e.currentTarget) closeModal();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape" && feedbackState !== "submitting") closeModal();
}
async function submitFeedback(choice: "yes" | "no") {
if (!canSubmit) return;
feedbackState = "submitting";
lastChoice = choice;
if (feedbackConfig.debug) {
feedbackState = "animating";
setTimeout(() => {
feedbackDone = true;
localStorage.setItem("dns-feedback-submitted", "1");
}, 1500);
return;
}
try {
const resp = await fetch(feedbackConfig.apiEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: currentUrl,
choice,
timestamp: new Date().toISOString(),
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
feedbackState = "animating";
setTimeout(() => {
feedbackDone = true;
localStorage.setItem("dns-feedback-submitted", "1");
}, 1500);
} catch (err) {
console.error("[DNS Feedback]", err);
feedbackState = "error";
setTimeout(() => {
feedbackState = "idle";
lastChoice = null;
}, 3000);
}
}
</script>
{#if !dismissed}
<div id="dns-warning-banner" class="sticky top-0 z-[100] w-full bg-gradient-to-r from-amber-50/95 via-amber-50/90 to-amber-50/95 dark:from-amber-950/80 dark:via-amber-950/75 dark:to-amber-950/80 backdrop-blur-md border-b border-amber-200/60 dark:border-amber-700/40 text-amber-900 dark:text-amber-100 px-4 py-3 text-sm text-center">
<div class="select-none flex items-center justify-center gap-2 max-w-[var(--page-width)] mx-auto flex-wrap">
<Icon icon="material-symbols:info-outline-rounded" class="pointer-events-none shrink-0 text-lg" aria-hidden="true" />
<span class="text-balance">本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。</span>
{#if showFeedback}
<button
onclick={openModal}
class="shrink-0 btn-plain scale-animation rounded-lg h-7 px-2.5 flex items-center gap-1 text-xs font-medium text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 active:scale-90"
aria-label="反馈"
>
<Icon icon="material-symbols:feedback-outline-rounded" class="text-sm" aria-hidden="true" />
<span>反馈</span>
</button>
{/if}
<!-- close button -->
<button
onclick={dismissBar}
class="select-auto shrink-0 btn-plain scale-animation rounded-lg w-7 h-7 flex items-center justify-center text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 active:scale-90"
aria-label="关闭通知"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
</div>
</div>
<!-- Modal -->
{#if isOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
role="presentation"
class="fixed inset-0 z-[200] bg-black/30 dark:bg-black/50 backdrop-blur-sm"
transition:fade={{ duration: 100 }}
onclick={handleOverlayClick}
aria-hidden="true"
></div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
role="dialog"
aria-modal="true"
aria-label="站点反馈"
class="fixed inset-0 z-[210] flex items-center justify-center p-4"
onkeydown={handleKeydown}
onclick={handleOverlayClick}
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={modalRef}
class="w-full max-w-sm rounded-[var(--radius-large)] border border-black/5 dark:border-white/5
bg-[var(--card-bg)] shadow-xl dark:shadow-none"
transition:fly={{ duration: 150, y: 6, easing: cubicOut }}
>
<!-- Header -->
<div class="flex items-center justify-between px-5 pt-5 pb-3 border-b border-[var(--line-color)]">
<h3 class="text-base font-semibold text-90">
DNS 污染反馈
</h3>
<button
onclick={closeModal}
class="-mr-1 btn-plain scale-animation w-8 h-8 rounded-lg flex items-center justify-center"
aria-label="关闭"
>
<Icon icon="material-symbols:close-rounded" class="text-xl" aria-hidden="true" />
</button>
</div>
<!-- Body -->
<div class="px-5 py-6">
{#if feedbackState === "animating"}
<div class="flex flex-col items-center gap-3 py-4">
<Icon
icon="material-symbols:check-circle-rounded"
class="text-4xl text-green-500 dark:text-green-400 success-icon"
aria-hidden="true"
/>
<p class="text-sm text-50 text-center">
感谢你的反馈!
</p>
</div>
{:else}
<p class="text-sm text-50 text-center mb-5">
本站是否能够在你所在地区被正常访问
</p>
{#if isError}
<p class="text-xs text-red-500 dark:text-red-400 text-center -mt-3 mb-4">提交失败,请稍后重试</p>
{/if}
<div class="flex gap-3 justify-center">
<button
onclick={() => submitFeedback("yes")}
disabled={!canSubmit}
class="feedback-option"
class:feedback-option--active={lastChoice === "yes" && feedbackState !== "idle"}
class:feedback-option--disabled={!canSubmit}
aria-label="可以访问"
>
<Icon icon="material-symbols:thumb-up-outline-rounded" class="text-lg" aria-hidden="true" />
<span></span>
</button>
<button
onclick={() => submitFeedback("no")}
disabled={!canSubmit}
class="feedback-option"
class:feedback-option--active={lastChoice === "no" && feedbackState !== "idle"}
class:feedback-option--disabled={!canSubmit}
aria-label="无法访问"
>
<Icon icon="material-symbols:thumb-down-outline-rounded" class="text-lg" aria-hidden="true" />
<span></span>
</button>
</div>
{/if}
</div>
</div>
</div>
{/if}
{/if}
<style>
.success-icon {
animation: success-pop 350ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes success-pop {
0% {
opacity: 0;
transform: scale(0.5);
}
60% {
transform: scale(1.15);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.feedback-option {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1.25rem;
border-radius: 0.75rem;
border: 1px solid transparent;
background: var(--btn-regular-bg);
color: var(--btn-content);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
user-select: none;
}
.feedback-option:hover:not(:disabled) {
background: var(--btn-regular-bg-hover);
border-color: var(--primary);
color: var(--primary);
}
.feedback-option:active:not(:disabled) {
background: var(--btn-regular-bg-active);
transform: scale(0.97);
}
.feedback-option:disabled {
cursor: default;
}
.feedback-option:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.feedback-option--disabled {
opacity: 0.5;
}
.feedback-option--active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
</style>
+10 -3
View File
@@ -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",
};
};
+30 -30
View File
@@ -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,
};
+2
View File
@@ -32,6 +32,8 @@ enum I18nKey {
author = "author",
publishedAt = "publishedAt",
license = "license",
dnsFeedbackError = "dnsFeedbackError",
}
export default I18nKey;
+2
View File
@@ -35,4 +35,6 @@ export const zh_CN: Translation = {
[Key.author]: "作者",
[Key.publishedAt]: "发布于",
[Key.license]: "许可协议",
[Key.dnsFeedbackError]: "提交失败",
};
+2 -24
View File
@@ -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
>
<ConfigCarrier></ConfigCarrier>
<div id="dns-warning-banner" class="hidden sticky top-0 z-[100] w-full bg-amber-50/90 dark:bg-amber-950/80 backdrop-blur-md border-b border-amber-200/60 dark:border-amber-700/40 text-amber-900 dark:text-amber-100 px-4 py-3 text-sm text-center">
<div class="select-none flex items-center justify-center gap-2 max-w-[var(--page-width)] mx-auto">
<Icon is:inline name="material-symbols:info-outline-rounded" class="pointer-events-none shrink-0 text-lg" aria-hidden="true"></Icon>
<span>本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。</span>
<button id="dns-warning-close" class="select-auto shrink-0 btn-plain scale-animation rounded-lg w-7 h-7 flex items-center justify-center text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 active:scale-90" aria-label="Close notice">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
</div>
</div>
<script>
(function() {
var banner = document.getElementById('dns-warning-banner');
var closeBtn = document.getElementById('dns-warning-close');
if (!banner || !closeBtn) return;
if (!localStorage.getItem('dns-warning-dismissed')) {
banner.classList.remove('hidden');
}
closeBtn.addEventListener('click', function() {
banner.classList.add('hidden');
localStorage.setItem('dns-warning-dismissed', '1');
});
})();
</script>
<DnsFeedbackBanner client:only="svelte" />
<slot />
<!-- increase the page height during page transition to prevent the scrolling animation from jumping -->
+2 -1
View File
@@ -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) {
+46 -11
View File
@@ -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 = [
+29 -27
View File
@@ -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: `<language>${siteConfig.lang}</language><follow_challenge><feedId>250504037866558464</feedId><userId>83370505718413312</userId></follow_challenge>`,
+10 -6
View File
@@ -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;
};
};
export type FeedbackConfig = {
enable: boolean;
apiEndpoint: string;
debug?: boolean;
};
+8 -5
View File
@@ -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<number> {
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;