From 69d128b6559ec0e976cf42a000f49cae4e0b5907 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 09:40:12 +0000 Subject: [PATCH 1/8] Initial plan From 47b43a719cd71b545acdd0da90fb28db94aa32fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 09:51:24 +0000 Subject: [PATCH 2/8] Changes before error encountered Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/b539c51c-4f2b-43eb-945a-c0b7cee3fa69 --- src/content/wiki/macos-intel-install.md | 2 +- src/layouts/Doc.astro | 3 + src/layouts/Post.astro | 5 +- src/scripts/enhanceMarkdownImages.ts | 102 ++++++++++++++++++++++++ src/styles/markdown.css | 35 ++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/scripts/enhanceMarkdownImages.ts diff --git a/src/content/wiki/macos-intel-install.md b/src/content/wiki/macos-intel-install.md index 23b4a98..5d161af 100644 --- a/src/content/wiki/macos-intel-install.md +++ b/src/content/wiki/macos-intel-install.md @@ -7,7 +7,7 @@ authors: 1. 下载 ZIP 压缩包。 2. 下载完成后,在 Finder 中双击压缩包进行解压。 - + 3. 打开新的 Finder 窗口,进入 `/Applications/` 目录: - 可在侧边栏直接选择(中文系统中显示为「应用程序」); - 或使用快捷键 Shift G,输入 `/Applications/` 后回车。 diff --git a/src/layouts/Doc.astro b/src/layouts/Doc.astro index 9ad8978..748a31a 100644 --- a/src/layouts/Doc.astro +++ b/src/layouts/Doc.astro @@ -224,6 +224,9 @@ const leftNavItems: NavItem[] = wiki - \ No newline at end of file + diff --git a/src/scripts/enhanceMarkdownImages.ts b/src/scripts/enhanceMarkdownImages.ts new file mode 100644 index 0000000..0918d49 --- /dev/null +++ b/src/scripts/enhanceMarkdownImages.ts @@ -0,0 +1,102 @@ +const imageDimensions: Record = { + "/features/subject-details.png": { width: 575, height: 1280 }, + "/features/subject-rating.png": { width: 575, height: 1280 }, + "/features/anime-schedule.png": { width: 575, height: 1280 }, + "/features/search-by-tag.png": { width: 575, height: 1280 }, + "/features/subject-collection.png": { width: 575, height: 1280 }, + "/features/home.png": { width: 575, height: 1280 }, + "/features/mediaselector-simple.png": { width: 1080, height: 2400 }, + "/features/mediaselector-detailed.png": { width: 1080, height: 2400 }, + "/features/episode.png": { width: 575, height: 1280 }, + "/features/episode-scrolled.png": { width: 575, height: 1280 }, + "/features/cache-episode.png": { width: 575, height: 1280 }, + "/features/cache-list.png": { width: 575, height: 1280 }, + "/features/player-fullscreen.png": { width: 2400, height: 1080 }, + "/features/pc-home.png": { width: 2966, height: 1576 }, + "/features/pc-search.png": { width: 2722, height: 1742 }, + "/features/pc-search-detail.png": { width: 2528, height: 1742 }, + "/features/danmaku-settings.png": { width: 2400, height: 1080 }, + "/features/theme-settings.png": { width: 1080, height: 2400 }, + "/features/media-preferences.png": { width: 575, height: 1280 }, + "/images/iOS-sideloadly.png": { width: 1053, height: 374 }, + "/images/iOS-ready.png": { width: 1053, height: 374 }, + "/images/iOS-done.png": { width: 1053, height: 374 }, + "/images/macos-intel.png": { width: 332, height: 440 }, + "/images/macos-security.png": { width: 504, height: 418 }, + "/images/macos-xattr.png": { width: 1400, height: 1326 }, + "/images/macos-open.png": { width: 500, height: 582 }, + "/images/win-font-1.png": { width: 1689, height: 951 }, + "/images/win-font-2.png": { width: 956, height: 571 }, + "/images/win-font-3.png": { width: 942, height: 557 }, +}; + +function isNumericDimension(value: string) { + return /^\d+(\.\d+)?$/.test(value.trim()); +} + +export function enhanceMarkdownImages() { + const images = document.querySelectorAll(".markdown-body img"); + + images.forEach((img) => { + if (img.dataset.enhancedImage === "true") { + return; + } + img.dataset.enhancedImage = "true"; + + const src = img.getAttribute("src"); + const dims = src ? imageDimensions[src] : undefined; + const widthAttr = img.getAttribute("width"); + const heightAttr = img.getAttribute("height"); + + if (!img.hasAttribute("loading")) { + img.setAttribute("loading", "lazy"); + } + + img.setAttribute("decoding", "async"); + img.classList.add("lazy-image", "is-loading"); + + if (dims) { + if (!widthAttr && !heightAttr) { + img.setAttribute("width", String(dims.width)); + img.setAttribute("height", String(dims.height)); + } else if (widthAttr && !heightAttr && isNumericDimension(widthAttr)) { + const displayWidth = Number(widthAttr); + if (!Number.isNaN(displayWidth) && displayWidth > 0) { + const displayHeight = Math.round((displayWidth * dims.height) / dims.width); + img.setAttribute("height", String(displayHeight)); + } + } else if (heightAttr && !widthAttr && isNumericDimension(heightAttr)) { + const displayHeight = Number(heightAttr); + if (!Number.isNaN(displayHeight) && displayHeight > 0) { + const displayWidth = Math.round((displayHeight * dims.width) / dims.height); + img.setAttribute("width", String(displayWidth)); + } + } + } + + const markLoaded = (hasError = false) => { + img.classList.remove("is-loading"); + img.classList.add(hasError ? "is-error" : "is-loaded"); + }; + + if (img.complete) { + markLoaded(img.naturalWidth === 0); + return; + } + + img.addEventListener( + "load", + () => { + markLoaded(false); + }, + { once: true } + ); + img.addEventListener( + "error", + () => { + markLoaded(true); + }, + { once: true } + ); + }); +} diff --git a/src/styles/markdown.css b/src/styles/markdown.css index 7cf0b07..a59caae 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -128,6 +128,41 @@ details .anchor { margin: 8px 0; } +.markdown-body img.lazy-image { + display: block; + border-radius: 10px; + background: #1f2937; + transition: + opacity 0.3s ease, + background-color 0.3s ease; +} + +.markdown-body img.lazy-image.is-loading { + animation: markdown-image-pulse 1.4s ease-in-out infinite; +} + +.markdown-body img.lazy-image.is-loaded { + animation: none; + background: transparent; +} + +.markdown-body img.lazy-image.is-error { + animation: none; + opacity: 0.6; + background: #111827; +} + +@keyframes markdown-image-pulse { + 0%, + 100% { + opacity: 0.75; + } + + 50% { + opacity: 1; + } +} + .markdown-body code, .markdown-body kbd, .markdown-body pre, From 0919ebffbbcf216d6fa50c17c5dcb469a63333f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 10:26:16 +0000 Subject: [PATCH 3/8] Changes before error encountered Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/4c0f56a9-86ff-478d-9ac5-62ac664b5d15 --- astro.config.mjs | 2 + src/plugins/rehypeMarkdownImages.ts | 129 +++++++++++++++++++++++++++ src/scripts/enhanceMarkdownImages.ts | 90 ++----------------- src/styles/markdown.css | 2 + 4 files changed, 141 insertions(+), 82 deletions(-) create mode 100644 src/plugins/rehypeMarkdownImages.ts diff --git a/astro.config.mjs b/astro.config.mjs index 1ff5762..ac365dd 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,6 +5,7 @@ import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "astro/config"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import { remarkAlert } from "remark-github-blockquote-alert"; +import rehypeMarkdownImages from "./src/plugins/rehypeMarkdownImages.ts"; // https://astro.build/config export default defineConfig({ @@ -27,6 +28,7 @@ export default defineConfig({ }, remarkPlugins: [remarkAlert], rehypePlugins: [ + rehypeMarkdownImages, // https://docs.astro.build/en/guides/markdown-content/#heading-ids-and-plugins [rehypeHeadingIds, { headingIdCompat: true }], [ diff --git a/src/plugins/rehypeMarkdownImages.ts b/src/plugins/rehypeMarkdownImages.ts new file mode 100644 index 0000000..87758ed --- /dev/null +++ b/src/plugins/rehypeMarkdownImages.ts @@ -0,0 +1,129 @@ +/** Minimal inline types for hast nodes (avoids external type dependency). */ +interface HastProperties { + [key: string]: unknown; + src?: string; + loading?: string; + decoding?: string; + width?: number | string; + height?: number | string; + className?: string | string[]; +} + +interface HastElement { + type: "element"; + tagName: string; + properties: HastProperties; + children: HastNode[]; +} + +interface HastParent { + type: string; + children: HastNode[]; +} + +type HastNode = HastElement | HastParent; + +const imageDimensions: Record = { + "/features/subject-details.png": { width: 575, height: 1280 }, + "/features/subject-rating.png": { width: 575, height: 1280 }, + "/features/anime-schedule.png": { width: 575, height: 1280 }, + "/features/search-by-tag.png": { width: 575, height: 1280 }, + "/features/subject-collection.png": { width: 575, height: 1280 }, + "/features/home.png": { width: 575, height: 1280 }, + "/features/mediaselector-simple.png": { width: 1080, height: 2400 }, + "/features/mediaselector-detailed.png": { width: 1080, height: 2400 }, + "/features/episode.png": { width: 575, height: 1280 }, + "/features/episode-scrolled.png": { width: 575, height: 1280 }, + "/features/cache-episode.png": { width: 575, height: 1280 }, + "/features/cache-list.png": { width: 575, height: 1280 }, + "/features/player-fullscreen.png": { width: 2400, height: 1080 }, + "/features/pc-home.png": { width: 2966, height: 1576 }, + "/features/pc-search.png": { width: 2722, height: 1742 }, + "/features/pc-search-detail.png": { width: 2528, height: 1742 }, + "/features/danmaku-settings.png": { width: 2400, height: 1080 }, + "/features/theme-settings.png": { width: 1080, height: 2400 }, + "/features/media-preferences.png": { width: 575, height: 1280 }, + "/images/iOS-sideloadly.png": { width: 1053, height: 374 }, + "/images/iOS-ready.png": { width: 1053, height: 374 }, + "/images/iOS-done.png": { width: 1053, height: 374 }, + "/images/macos-intel.png": { width: 332, height: 440 }, + "/images/macos-security.png": { width: 504, height: 418 }, + "/images/macos-xattr.png": { width: 1400, height: 1326 }, + "/images/macos-open.png": { width: 500, height: 582 }, + "/images/win-font-1.png": { width: 1689, height: 951 }, + "/images/win-font-2.png": { width: 956, height: 571 }, + "/images/win-font-3.png": { width: 942, height: 557 }, +}; + +function visitElements(node: HastNode, visitor: (element: HastElement) => void): void { + if (node.type === "element") { + visitor(node as HastElement); + } + if ("children" in node && Array.isArray(node.children)) { + for (const child of node.children) { + visitElements(child, visitor); + } + } +} + +/** + * Rehype plugin that enhances markdown `` elements at build time: + * - Adds `loading="lazy"` and `decoding="async"` + * - Injects missing `width`/`height` (and infers the missing side for known images) + * - Adds `lazy-image is-loading` CSS classes so the skeleton is server-rendered + * + * SVG images are skipped for the skeleton treatment (loading/decoding still added). + */ +export default function rehypeMarkdownImages() { + return (tree: HastNode) => { + visitElements(tree, (node) => { + if (node.tagName !== "img") return; + + const props = node.properties; + const src = typeof props.src === "string" ? props.src : undefined; + const isSvg = src?.toLowerCase().endsWith(".svg") === true; + + // Add loading="lazy" and decoding="async" to all images + if (!props.loading) { + props.loading = "lazy"; + } + props.decoding = "async"; + + // SVGs don't need skeleton treatment + if (isSvg) return; + + // Add/infer width and height for known images + const dims = src ? imageDimensions[src] : undefined; + if (dims) { + const hasWidth = props.width != null && props.width !== ""; + const hasHeight = props.height != null && props.height !== ""; + + if (!hasWidth && !hasHeight) { + props.width = dims.width; + props.height = dims.height; + } else if (hasWidth && !hasHeight) { + const w = Number(props.width); + if (!Number.isNaN(w) && w > 0) { + props.height = Math.round((w * dims.height) / dims.width); + } + } else if (!hasWidth && hasHeight) { + const h = Number(props.height); + if (!Number.isNaN(h) && h > 0) { + props.width = Math.round((h * dims.width) / dims.height); + } + } + } + + // Add lazy-image and is-loading classes for skeleton animation + const existing = Array.isArray(props.className) + ? (props.className as string[]).filter((c) => typeof c === "string") + : props.className + ? [String(props.className)] + : []; + + if (!existing.includes("lazy-image")) { + props.className = [...existing, "lazy-image", "is-loading"]; + } + }); + }; +} diff --git a/src/scripts/enhanceMarkdownImages.ts b/src/scripts/enhanceMarkdownImages.ts index 0918d49..5c9914d 100644 --- a/src/scripts/enhanceMarkdownImages.ts +++ b/src/scripts/enhanceMarkdownImages.ts @@ -1,41 +1,10 @@ -const imageDimensions: Record = { - "/features/subject-details.png": { width: 575, height: 1280 }, - "/features/subject-rating.png": { width: 575, height: 1280 }, - "/features/anime-schedule.png": { width: 575, height: 1280 }, - "/features/search-by-tag.png": { width: 575, height: 1280 }, - "/features/subject-collection.png": { width: 575, height: 1280 }, - "/features/home.png": { width: 575, height: 1280 }, - "/features/mediaselector-simple.png": { width: 1080, height: 2400 }, - "/features/mediaselector-detailed.png": { width: 1080, height: 2400 }, - "/features/episode.png": { width: 575, height: 1280 }, - "/features/episode-scrolled.png": { width: 575, height: 1280 }, - "/features/cache-episode.png": { width: 575, height: 1280 }, - "/features/cache-list.png": { width: 575, height: 1280 }, - "/features/player-fullscreen.png": { width: 2400, height: 1080 }, - "/features/pc-home.png": { width: 2966, height: 1576 }, - "/features/pc-search.png": { width: 2722, height: 1742 }, - "/features/pc-search-detail.png": { width: 2528, height: 1742 }, - "/features/danmaku-settings.png": { width: 2400, height: 1080 }, - "/features/theme-settings.png": { width: 1080, height: 2400 }, - "/features/media-preferences.png": { width: 575, height: 1280 }, - "/images/iOS-sideloadly.png": { width: 1053, height: 374 }, - "/images/iOS-ready.png": { width: 1053, height: 374 }, - "/images/iOS-done.png": { width: 1053, height: 374 }, - "/images/macos-intel.png": { width: 332, height: 440 }, - "/images/macos-security.png": { width: 504, height: 418 }, - "/images/macos-xattr.png": { width: 1400, height: 1326 }, - "/images/macos-open.png": { width: 500, height: 582 }, - "/images/win-font-1.png": { width: 1689, height: 951 }, - "/images/win-font-2.png": { width: 956, height: 571 }, - "/images/win-font-3.png": { width: 942, height: 557 }, -}; - -function isNumericDimension(value: string) { - return /^\d+(\.\d+)?$/.test(value.trim()); -} - +/** + * Attaches load/error listeners to markdown images that were given the + * `lazy-image is-loading` classes by the server-side rehype plugin, so the + * skeleton animation is dismissed once each image settles. + */ export function enhanceMarkdownImages() { - const images = document.querySelectorAll(".markdown-body img"); + const images = document.querySelectorAll(".markdown-body img.lazy-image"); images.forEach((img) => { if (img.dataset.enhancedImage === "true") { @@ -43,37 +12,6 @@ export function enhanceMarkdownImages() { } img.dataset.enhancedImage = "true"; - const src = img.getAttribute("src"); - const dims = src ? imageDimensions[src] : undefined; - const widthAttr = img.getAttribute("width"); - const heightAttr = img.getAttribute("height"); - - if (!img.hasAttribute("loading")) { - img.setAttribute("loading", "lazy"); - } - - img.setAttribute("decoding", "async"); - img.classList.add("lazy-image", "is-loading"); - - if (dims) { - if (!widthAttr && !heightAttr) { - img.setAttribute("width", String(dims.width)); - img.setAttribute("height", String(dims.height)); - } else if (widthAttr && !heightAttr && isNumericDimension(widthAttr)) { - const displayWidth = Number(widthAttr); - if (!Number.isNaN(displayWidth) && displayWidth > 0) { - const displayHeight = Math.round((displayWidth * dims.height) / dims.width); - img.setAttribute("height", String(displayHeight)); - } - } else if (heightAttr && !widthAttr && isNumericDimension(heightAttr)) { - const displayHeight = Number(heightAttr); - if (!Number.isNaN(displayHeight) && displayHeight > 0) { - const displayWidth = Math.round((displayHeight * dims.width) / dims.height); - img.setAttribute("width", String(displayWidth)); - } - } - } - const markLoaded = (hasError = false) => { img.classList.remove("is-loading"); img.classList.add(hasError ? "is-error" : "is-loaded"); @@ -84,19 +22,7 @@ export function enhanceMarkdownImages() { return; } - img.addEventListener( - "load", - () => { - markLoaded(false); - }, - { once: true } - ); - img.addEventListener( - "error", - () => { - markLoaded(true); - }, - { once: true } - ); + img.addEventListener("load", () => markLoaded(false), { once: true }); + img.addEventListener("error", () => markLoaded(true), { once: true }); }); } diff --git a/src/styles/markdown.css b/src/styles/markdown.css index a59caae..3d34f91 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -139,6 +139,8 @@ details .anchor { .markdown-body img.lazy-image.is-loading { animation: markdown-image-pulse 1.4s ease-in-out infinite; + /* Ensure a visible skeleton area even when intrinsic dimensions are unknown. */ + min-height: 120px; } .markdown-body img.lazy-image.is-loaded { From ffc1f4983211e9cfb7a7e9023804fa209e8ccf6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 10:32:24 +0000 Subject: [PATCH 4/8] Changes before error encountered Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/5a33fda1-8142-463b-83a4-bbdf18cff7e0 --- src/styles/markdown.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/markdown.css b/src/styles/markdown.css index 3d34f91..826a959 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -130,7 +130,6 @@ details .anchor { .markdown-body img.lazy-image { display: block; - border-radius: 10px; background: #1f2937; transition: opacity 0.3s ease, From 407f646168ead5a384fd8ca1f6f86053db3b9ded Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 12:41:25 +0000 Subject: [PATCH 5/8] Use WeakSet to track enhanced images instead of data attribute Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/fcdf4390-451a-4af0-839d-09acdab43f61 Co-authored-by: nick-cjyx9 <119087246+nick-cjyx9@users.noreply.github.com> --- src/scripts/enhanceMarkdownImages.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scripts/enhanceMarkdownImages.ts b/src/scripts/enhanceMarkdownImages.ts index 5c9914d..744c3c9 100644 --- a/src/scripts/enhanceMarkdownImages.ts +++ b/src/scripts/enhanceMarkdownImages.ts @@ -1,3 +1,5 @@ +const enhanced = new WeakSet(); + /** * Attaches load/error listeners to markdown images that were given the * `lazy-image is-loading` classes by the server-side rehype plugin, so the @@ -7,10 +9,10 @@ export function enhanceMarkdownImages() { const images = document.querySelectorAll(".markdown-body img.lazy-image"); images.forEach((img) => { - if (img.dataset.enhancedImage === "true") { + if (enhanced.has(img)) { return; } - img.dataset.enhancedImage = "true"; + enhanced.add(img); const markLoaded = (hasError = false) => { img.classList.remove("is-loading"); From 3bfa43083dceb29753ed56b7a422d3c7b19055e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 13:04:00 +0000 Subject: [PATCH 6/8] =?UTF-8?q?Add=20fade-in=20opacity=20animation=20when?= =?UTF-8?q?=20image=20loads=20(skeleton=20=E2=86=92=20loaded)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/733c5fd9-448f-4ee4-ab3f-34aaf0d95d2d Co-authored-by: nick-cjyx9 <119087246+nick-cjyx9@users.noreply.github.com> --- src/styles/markdown.css | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/styles/markdown.css b/src/styles/markdown.css index 826a959..865d416 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -131,9 +131,6 @@ details .anchor { .markdown-body img.lazy-image { display: block; background: #1f2937; - transition: - opacity 0.3s ease, - background-color 0.3s ease; } .markdown-body img.lazy-image.is-loading { @@ -143,8 +140,8 @@ details .anchor { } .markdown-body img.lazy-image.is-loaded { - animation: none; background: transparent; + animation: markdown-image-fade-in 0.35s ease forwards; } .markdown-body img.lazy-image.is-error { @@ -164,6 +161,16 @@ details .anchor { } } +@keyframes markdown-image-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + .markdown-body code, .markdown-body kbd, .markdown-body pre, From da4e66413e4fb92182ae5479f15380571054dd36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 13:18:08 +0000 Subject: [PATCH 7/8] Fix SVG dimension injection; move aspect-ratio inference before SVG early-return Agent-Logs-Url: https://github.com/open-ani/ani-website/sessions/bed598a5-be3b-4ec7-96d0-bf4864f51988 Co-authored-by: nick-cjyx9 <119087246+nick-cjyx9@users.noreply.github.com> --- src/plugins/rehypeMarkdownImages.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/plugins/rehypeMarkdownImages.ts b/src/plugins/rehypeMarkdownImages.ts index 87758ed..4688363 100644 --- a/src/plugins/rehypeMarkdownImages.ts +++ b/src/plugins/rehypeMarkdownImages.ts @@ -24,6 +24,7 @@ interface HastParent { type HastNode = HastElement | HastParent; const imageDimensions: Record = { + "/images/animeko.svg": { width: 1280, height: 640 }, "/features/subject-details.png": { width: 575, height: 1280 }, "/features/subject-rating.png": { width: 575, height: 1280 }, "/features/anime-schedule.png": { width: 575, height: 1280 }, @@ -69,10 +70,14 @@ function visitElements(node: HastNode, visitor: (element: HastElement) => void): /** * Rehype plugin that enhances markdown `` elements at build time: * - Adds `loading="lazy"` and `decoding="async"` - * - Injects missing `width`/`height` (and infers the missing side for known images) + * - Injects missing `width`/`height` for known images (including SVGs). + * When only one dimension is already present the other is inferred from the + * known aspect ratio — this covers the inline `` elements + * in about.md where the browser needs both values to reserve space (CLS). * - Adds `lazy-image is-loading` CSS classes so the skeleton is server-rendered * - * SVG images are skipped for the skeleton treatment (loading/decoding still added). + * SVG images are skipped only for the skeleton class treatment; they still + * receive `loading`, `decoding`, and dimension attributes. */ export default function rehypeMarkdownImages() { return (tree: HastNode) => { @@ -89,10 +94,10 @@ export default function rehypeMarkdownImages() { } props.decoding = "async"; - // SVGs don't need skeleton treatment - if (isSvg) return; - - // Add/infer width and height for known images + // Inject width/height for ALL known images (including SVGs). + // When only one dimension is supplied (e.g. width="200" in about.md inline HTML) + // the missing value is inferred from the known aspect ratio so the browser can + // reserve the correct amount of space before the image loads (prevents CLS). const dims = src ? imageDimensions[src] : undefined; if (dims) { const hasWidth = props.width != null && props.width !== ""; @@ -114,6 +119,9 @@ export default function rehypeMarkdownImages() { } } + // SVGs don't need skeleton treatment — they render immediately. + if (isSvg) return; + // Add lazy-image and is-loading classes for skeleton animation const existing = Array.isArray(props.className) ? (props.className as string[]).filter((c) => typeof c === "string") From 95fc2adb53b54c0b0fd19ce943757074f1328638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?NickChen=E3=83=B0?= <119087246+nick-cjyx9@users.noreply.github.com> Date: Mon, 11 May 2026 21:49:57 +0800 Subject: [PATCH 8/8] :lipstick: fix slop --- public/images/default-avatar.jpg | Bin 0 -> 13818 bytes src/layouts/Doc.astro | 1 + src/layouts/Layout.astro | 1 + src/plugins/rehypeMarkdownImages.ts | 97 +++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 public/images/default-avatar.jpg diff --git a/public/images/default-avatar.jpg b/public/images/default-avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1d904a316dbb22ef7067e14decb0e9fba6daff4a GIT binary patch literal 13818 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 ToP}FZckMN6e>gX}o&0|Q67&Zv literal 0 HcmV?d00001 diff --git a/src/layouts/Doc.astro b/src/layouts/Doc.astro index 748a31a..d7e62e0 100644 --- a/src/layouts/Doc.astro +++ b/src/layouts/Doc.astro @@ -118,6 +118,7 @@ const leftNavItems: NavItem[] = wiki alt={author} class="w-8 h-8 rounded-full bg-gray-600" loading="lazy" + onerror="this.onerror=null;this.src='/images/default-avatar.jpg';" /> ))} diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index d2c28ca..2d8bc82 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -24,6 +24,7 @@ const { title } = Astro.props; content="Animeko, 弹幕, 追番, 全自动BT, 数据源聚合, 离线缓存, Bangumi, 弹幕云过滤, 开源, 动漫, 收藏同步"> + {title} diff --git a/src/plugins/rehypeMarkdownImages.ts b/src/plugins/rehypeMarkdownImages.ts index 4688363..71a3edd 100644 --- a/src/plugins/rehypeMarkdownImages.ts +++ b/src/plugins/rehypeMarkdownImages.ts @@ -21,7 +21,16 @@ interface HastParent { children: HastNode[]; } -type HastNode = HastElement | HastParent; +interface HastRaw { + type: "raw"; + value: string; +} + +type HastNode = HastElement | HastParent | HastRaw; + +function isRawNode(node: HastNode): node is HastRaw { + return node.type === "raw" && "value" in node; +} const imageDimensions: Record = { "/images/animeko.svg": { width: 1280, height: 640 }, @@ -56,13 +65,79 @@ const imageDimensions: Record = { "/images/win-font-3.png": { width: 942, height: 557 }, }; -function visitElements(node: HastNode, visitor: (element: HastElement) => void): void { +function titleCase(value: string): string { + return value.replace(/\b\w/g, (char) => char.toUpperCase()); +} + +function inferAltFromSrc(src: string): string | null { + const cleaned = src.split(/[?#]/)[0]; + const filename = cleaned.split("/").pop(); + if (!filename) return null; + + const base = filename.replace(/\.[a-z0-9]+$/i, ""); + const spaced = base.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim(); + if (!spaced) return null; + + return titleCase(spaced); +} + +function parseImgFromRaw(value: string): HastElement | null { + const trimmed = value.trim(); + const match = trimmed.match(/^]*?)\s*\/?>$/i); + if (!match) return null; + + const props: HastProperties = {}; + const attrs = match[1]; + const attrRegex = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g; + + for (let attrMatch = attrRegex.exec(attrs); attrMatch; attrMatch = attrRegex.exec(attrs)) { + const key = attrMatch[1]; + const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? ""; + + if (key === "class") { + props.className = rawValue.split(/\s+/).filter(Boolean); + continue; + } + + if (key === "width" || key === "height") { + const numeric = Number(rawValue); + props[key] = Number.isNaN(numeric) ? rawValue : numeric; + continue; + } + + props[key] = rawValue; + } + + return { + type: "element", + tagName: "img", + properties: props, + children: [], + }; +} + +function visitElements( + node: HastNode, + visitor: (element: HastElement) => void, + parent?: HastParent, + index?: number +): void { + if (isRawNode(node)) { + const parsed = parseImgFromRaw(node.value); + if (parsed && parent && typeof index === "number") { + parent.children[index] = parsed; + visitor(parsed); + return; + } + } + if (node.type === "element") { visitor(node as HastElement); } + if ("children" in node && Array.isArray(node.children)) { - for (const child of node.children) { - visitElements(child, visitor); + for (let i = 0; i < node.children.length; i += 1) { + visitElements(node.children[i], visitor, node, i); } } } @@ -84,9 +159,9 @@ export default function rehypeMarkdownImages() { visitElements(tree, (node) => { if (node.tagName !== "img") return; - const props = node.properties; + const props = node.properties ?? {}; + node.properties = props; const src = typeof props.src === "string" ? props.src : undefined; - const isSvg = src?.toLowerCase().endsWith(".svg") === true; // Add loading="lazy" and decoding="async" to all images if (!props.loading) { @@ -94,6 +169,13 @@ export default function rehypeMarkdownImages() { } props.decoding = "async"; + if (!props.alt && src) { + const inferredAlt = inferAltFromSrc(src); + if (inferredAlt) { + props.alt = inferredAlt; + } + } + // Inject width/height for ALL known images (including SVGs). // When only one dimension is supplied (e.g. width="200" in about.md inline HTML) // the missing value is inferred from the known aspect ratio so the browser can @@ -119,9 +201,6 @@ export default function rehypeMarkdownImages() { } } - // SVGs don't need skeleton treatment — they render immediately. - if (isSvg) return; - // Add lazy-image and is-loading classes for skeleton animation const existing = Array.isArray(props.className) ? (props.className as string[]).filter((c) => typeof c === "string")