From ca7af28a8963cd1df45f38e1a72384d39e337623 Mon Sep 17 00:00:00 2001 From: Samera2022 Date: Sat, 3 Jan 2026 23:36:42 +0800 Subject: [PATCH 1/4] update --- src/components/widget/CategoryNode.astro | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/components/widget/CategoryNode.astro diff --git a/src/components/widget/CategoryNode.astro b/src/components/widget/CategoryNode.astro new file mode 100644 index 0000000000..8816d28f77 --- /dev/null +++ b/src/components/widget/CategoryNode.astro @@ -0,0 +1,87 @@ +--- +import type { Category } from "../../utils/content-utils"; + +interface Props { + node: Category; + depth?: number; + hasExpandableSibling?: boolean; +} + +const { node, depth = 0, hasExpandableSibling = false } = Astro.props; +const INDENT_PER_LEVEL = 12; +const indentPx = depth * INDENT_PER_LEVEL; +const hasChildren = node.children.length > 0; +--- + +{hasChildren ? ( +
+
+ + + {node.count > 0 && ( +
+ {node.count} +
+ )} +
+
+ {node.children.map((child) => ( + n.children.length > 0)} + /> + ))} +
+
+
+) : ( +
+ + + +
+)} + + + + From 921c886f70131e9e12c1441ae2bc3fa051e5fd17 Mon Sep 17 00:00:00 2001 From: Samera2022 Date: Sun, 4 Jan 2026 12:02:18 +0800 Subject: [PATCH 2/4] feat: layerable category --- src/utils/content-utils.ts | 109 ++++++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/src/utils/content-utils.ts b/src/utils/content-utils.ts index ca43516dca..96d695a0ea 100644 --- a/src/utils/content-utils.ts +++ b/src/utils/content-utils.ts @@ -73,42 +73,99 @@ export async function getTagList(): Promise { } export type Category = { - name: string; - count: number; + name: string; // current segment name + fullPath: string; // joined path like "Java/MouseMacros" + count: number; // includes children url: string; + children: Category[]; +}; + +type CategoryNodeInternal = Category & { + childrenMap: Map; }; +function createCategoryNode( + name: string, + fullPath: string, +): CategoryNodeInternal { + return { + name, + fullPath, + count: 0, + url: getCategoryUrl(fullPath), + children: [], + childrenMap: new Map(), + }; +} + +function normalizeCategory(raw: unknown): string { + if (!raw) return ""; + if (typeof raw === "string") return raw.trim(); + return String(raw).trim(); +} + +function addCategoryToTree( + root: Map, + segments: string[], + uncategorizedLabel: string, +) { + // If no valid segments, treat as uncategorized + if (segments.length === 0) { + const key = uncategorizedLabel; + const existing = root.get(key) ?? createCategoryNode(key, key); + existing.count += 1; + root.set(key, existing); + return; + } + + let currentMap = root; + let currentPath = ""; + + segments.forEach((segment) => { + currentPath = currentPath ? `${currentPath}/${segment}` : segment; + let node = currentMap.get(segment); + if (!node) { + node = createCategoryNode(segment, currentPath); + currentMap.set(segment, node); + } + node.count += 1; // accumulate counts on all levels + currentMap = node.childrenMap; + }); +} + +function mapToSortedCategories( + map: Map, +): Category[] { + const nodes = Array.from(map.values()).sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()), + ); + + return nodes.map((node) => ({ + name: node.name, + fullPath: node.fullPath, + count: node.count, + url: node.url, + children: mapToSortedCategories(node.childrenMap), + })); +} + export async function getCategoryList(): Promise { const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => { return import.meta.env.PROD ? data.draft !== true : true; }); - const count: { [key: string]: number } = {}; - allBlogPosts.forEach((post: { data: { category: string | null } }) => { - if (!post.data.category) { - const ucKey = i18n(I18nKey.uncategorized); - count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1; - return; - } - const categoryName = - typeof post.data.category === "string" - ? post.data.category.trim() - : String(post.data.category).trim(); + const uncategorizedLabel = i18n(I18nKey.uncategorized); + const rootMap = new Map(); - count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1; - }); + allBlogPosts.forEach((post: { data: { category: string | null } }) => { + const normalized = normalizeCategory(post.data.category); + const segments = normalized + .split("/") + .map((s) => s.trim()) + .filter(Boolean); - const lst = Object.keys(count).sort((a, b) => { - return a.toLowerCase().localeCompare(b.toLowerCase()); + addCategoryToTree(rootMap, segments, uncategorizedLabel); }); - const ret: Category[] = []; - for (const c of lst) { - ret.push({ - name: c, - count: count[c], - url: getCategoryUrl(c), - }); - } - return ret; + return mapToSortedCategories(rootMap); } From 21539b9980e782048f56b3a228314d711ae6cafd Mon Sep 17 00:00:00 2001 From: Samera2022 Date: Sun, 4 Jan 2026 12:08:33 +0800 Subject: [PATCH 3/4] feat: layerable category --- src/components/widget/Categories.astro | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/widget/Categories.astro b/src/components/widget/Categories.astro index b44e9da965..820f3e590a 100644 --- a/src/components/widget/Categories.astro +++ b/src/components/widget/Categories.astro @@ -2,10 +2,11 @@ import I18nKey from "../../i18n/i18nKey"; import { i18n } from "../../i18n/translation"; import { getCategoryList } from "../../utils/content-utils"; -import ButtonLink from "../control/ButtonLink.astro"; import WidgetLayout from "./WidgetLayout.astro"; +import CategoryNode from "./CategoryNode.astro"; const categories = await getCategoryList(); +const rootHasExpandable = categories.some((c) => c.children.length > 0); const COLLAPSED_HEIGHT = "7.5rem"; const COLLAPSE_THRESHOLD = 5; @@ -23,13 +24,7 @@ const style = Astro.props.style; - {categories.map((c) => - - {c.name.trim()} - - )} + {categories.map((category) => ( + + ))} \ No newline at end of file From 9738cace27cf16530e6bd6879f5b0cdb1fa41aab Mon Sep 17 00:00:00 2001 From: Samera2022 Date: Mon, 5 Jan 2026 15:38:45 +0800 Subject: [PATCH 4/4] untested --- src/components/ArchivePanel.svelte | 4 ++-- src/content/config.ts | 2 +- src/pages/archive.astro | 2 +- src/plugins/remark-directive-rehype.js | 2 +- src/utils/url-utils.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ArchivePanel.svelte b/src/components/ArchivePanel.svelte index 22939c254c..7d84299a90 100644 --- a/src/components/ArchivePanel.svelte +++ b/src/components/ArchivePanel.svelte @@ -5,8 +5,8 @@ import I18nKey from "../i18n/i18nKey"; import { i18n } from "../i18n/translation"; import { getPostUrlBySlug } from "../utils/url-utils"; -export let tags: string[]; -export let categories: string[]; +export let tags: string[] = []; +export let categories: string[] = []; export let sortedPosts: Post[] = []; const params = new URLSearchParams(window.location.search); diff --git a/src/content/config.ts b/src/content/config.ts index 8bc07fe9e9..1f26a14da9 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -9,7 +9,7 @@ const postsCollection = defineCollection({ description: z.string().optional().default(""), image: z.string().optional().default(""), tags: z.array(z.string()).optional().default([]), - category: z.string().optional().nullable().default(""), + category: z.string().optional().default(""), lang: z.string().optional().default(""), /* For internal use */ diff --git a/src/pages/archive.astro b/src/pages/archive.astro index 90ede2429a..c591e973f3 100644 --- a/src/pages/archive.astro +++ b/src/pages/archive.astro @@ -9,6 +9,6 @@ const sortedPostsList = await getSortedPostsList(); --- - + diff --git a/src/plugins/remark-directive-rehype.js b/src/plugins/remark-directive-rehype.js index 174cceb9a1..1da8c5a5ea 100644 --- a/src/plugins/remark-directive-rehype.js +++ b/src/plugins/remark-directive-rehype.js @@ -2,7 +2,7 @@ import { h } from "hastscript"; import { visit } from "unist-util-visit"; export function parseDirectiveNode() { - return (tree, { _data }) => { + return (tree, _file) => { visit(tree, (node) => { if ( node.type === "containerDirective" || diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts index 956050bbc2..9bf2b931dc 100644 --- a/src/utils/url-utils.ts +++ b/src/utils/url-utils.ts @@ -28,7 +28,7 @@ export function getCategoryUrl(category: string | null): string { category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase() ) return url("/archive/?uncategorized=true"); - return url(`/archive/?category=${encodeURIComponent(category.trim())}`); + return url(`/categories/${encodeURIComponent(category.trim())}/`); } export function getDir(path: string): string {