mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 01:00:04 -04:00
chore: format & feedback beautification
This commit is contained in:
+2
-1
@@ -35,4 +35,5 @@ yarn.lock
|
||||
# .obsidian
|
||||
.cache
|
||||
|
||||
build.log
|
||||
build.log
|
||||
.traces
|
||||
@@ -99,3 +99,4 @@ src/
|
||||
- Biome 不处理 CSS 文件(在 `biome.json` 中排除)
|
||||
- Svelte 组件使用 Svelte 5 语法
|
||||
- 主题色基于 CSS 变量 `--hue` 动态计算
|
||||
- 不要执行构建,除非特别要求
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
|
||||
import { siteConfig } from "../config";
|
||||
|
||||
---
|
||||
|
||||
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ enum I18nKey {
|
||||
author = "author",
|
||||
publishedAt = "publishedAt",
|
||||
license = "license",
|
||||
|
||||
dnsFeedbackError = "dnsFeedbackError",
|
||||
}
|
||||
|
||||
export default I18nKey;
|
||||
|
||||
@@ -35,4 +35,6 @@ export const zh_CN: Translation = {
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "发布于",
|
||||
[Key.license]: "许可协议",
|
||||
|
||||
[Key.dnsFeedbackError]: "提交失败",
|
||||
};
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user