From 29a9fa92f62a60aa77e3e72b43047643a1417c33 Mon Sep 17 00:00:00 2001 From: walishcs Date: Mon, 17 Aug 2026 19:43:46 +0800 Subject: [PATCH 1/5] fix: improve blog navigation and markdown rendering --- keystatic.config.ts | 6 ++++- src/components/BlogDirectory.tsx | 5 +++- src/lib/blog-directory.test.ts | 10 +++++++- src/lib/blog-directory.ts | 2 ++ src/lib/blog-markdown.test.ts | 40 ++++++++++++++++++++++++++++++++ src/lib/blog-markdown.ts | 19 +++++++++++++++ src/pages/blog/[id].astro | 15 +++++++++--- 7 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 src/lib/blog-markdown.test.ts create mode 100644 src/lib/blog-markdown.ts diff --git a/keystatic.config.ts b/keystatic.config.ts index b0d2360..ebbd728 100644 --- a/keystatic.config.ts +++ b/keystatic.config.ts @@ -358,7 +358,11 @@ export default config({ itemLabel: (props) => props.value || 'Tag', }), cover: imageField('Cover image', 'blog'), - body: fields.markdoc({ label: 'Body' }), + body: fields.markdoc({ + label: 'Body', + description: + 'The title is displayed automatically. Start article sections with Heading 2.', + }), }, }), projects: collection({ diff --git a/src/components/BlogDirectory.tsx b/src/components/BlogDirectory.tsx index dcab99e..cf3fa91 100644 --- a/src/components/BlogDirectory.tsx +++ b/src/components/BlogDirectory.tsx @@ -6,13 +6,16 @@ import { import { useMemo, useState } from 'react'; import { ContentCardGrid } from '@/components/ContentCardGrid'; import { + DEFAULT_BLOG_DIRECTORY_MODE, groupBlogPosts, type BlogDirectoryMode, type BlogDirectoryPost, } from '@/lib/blog-directory'; export function BlogDirectory({ posts }: { posts: BlogDirectoryPost[] }) { - const [mode, setMode] = useState('tag'); + const [mode, setMode] = useState( + DEFAULT_BLOG_DIRECTORY_MODE, + ); const groups = useMemo(() => groupBlogPosts(posts, mode), [mode, posts]); return ( diff --git a/src/lib/blog-directory.test.ts b/src/lib/blog-directory.test.ts index a9ea6a9..4456207 100644 --- a/src/lib/blog-directory.test.ts +++ b/src/lib/blog-directory.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { groupBlogPosts, type BlogDirectoryPost } from './blog-directory'; +import { + DEFAULT_BLOG_DIRECTORY_MODE, + groupBlogPosts, + type BlogDirectoryPost, +} from './blog-directory'; const posts: BlogDirectoryPost[] = [ { @@ -37,6 +41,10 @@ const posts: BlogDirectoryPost[] = [ ]; describe('blog directory grouping', () => { + it('defaults to grouping posts by year', () => { + expect(DEFAULT_BLOG_DIRECTORY_MODE).toBe('year'); + }); + it('uses only the first tag, sorts groups, and leaves untagged last', () => { const groups = groupBlogPosts(posts, 'tag'); diff --git a/src/lib/blog-directory.ts b/src/lib/blog-directory.ts index 53d515c..dda94ca 100644 --- a/src/lib/blog-directory.ts +++ b/src/lib/blog-directory.ts @@ -1,5 +1,7 @@ export type BlogDirectoryMode = 'tag' | 'year'; +export const DEFAULT_BLOG_DIRECTORY_MODE: BlogDirectoryMode = 'year'; + export interface BlogDirectoryPost { href: string; title: string; diff --git a/src/lib/blog-markdown.test.ts b/src/lib/blog-markdown.test.ts new file mode 100644 index 0000000..2d5b3ff --- /dev/null +++ b/src/lib/blog-markdown.test.ts @@ -0,0 +1,40 @@ +import { Markdown } from '@astryxdesign/core/Markdown'; +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import { prepareBlogMarkdown } from './blog-markdown'; + +describe('prepareBlogMarkdown', () => { + it('removes a leading heading that duplicates the page title', () => { + expect( + prepareBlogMarkdown( + '# An article title\n\n## First section\n\nBody text.', + 'An article title', + ), + ).toBe('## First section\n\nBody text.'); + }); + + it('keeps a leading heading with different content', () => { + const body = '# A distinct heading\n\nBody text.'; + + expect(prepareBlogMarkdown(body, 'The page title')).toBe(body); + }); + + it('handles empty content', () => { + expect(prepareBlogMarkdown(undefined, 'The page title')).toBe(''); + }); + + it('renders article structure with Astryx Markdown', () => { + const html = renderToStaticMarkup( + createElement( + Markdown, + { headingLevelStart: 1 }, + '## Section\n\n- Bullet\n\n1. Numbered', + ), + ); + + expect(html).toContain(' ) } -
+ + {body} + From 01d5908422f15c360e4828d2d4fed2c0e823eb96 Mon Sep 17 00:00:00 2001 From: walishcs Date: Mon, 17 Aug 2026 19:44:08 +0800 Subject: [PATCH 2/5] chore: upgrade Astryx to 0.4.3 --- AGENTS.md | 13 +++++------ package-lock.json | 56 +++++++++++++++++++++++------------------------ package.json | 12 +++++----- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 652d22a..c080fa7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ -Astryx v0.3.0 · 155 components +Astryx v0.4.3 · 156 components CLI: run every command as `npx astryx ` (shown below as `astryx ...`). SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled: @@ -55,17 +55,16 @@ WORKFLOW — discover, don't guess. Before writing UI: RULES: -- No
— components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav. -- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`). -- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only. -- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration. +- No
— components do all layout/spacing, page frame included. +- Frame first: read `astryx docs layout` before writing any page or screen — page frame, region widths, breakpoint behavior. +- Dense data = rows (Table, List/Item), never Card-wrapped list items; Card is for standalone widgets. Status = StatusDot/Token; Badge = counts only. - Custom styling: component props first; else style/className with tokens — var(--color-_|--spacing-_|--radius-\*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.) -- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-\* in :root. +- Tokens for every value (`astryx docs tokens`). Brand/accent belongs in the theme (`astryx theme list` / `theme add `, or `astryx theme template` for a custom one) — never override --color-\* in :root. - SELF-CHECK before you finish: re-read the file and replace any raw
/ layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-_|--spacing-_|…)). If unsure a component/prop exists, run `astryx component ` / `astryx search ""`; don't hand-roll CSS. MORE CLI: search "" find any component / hook / doc / template / block -component --list 155 components by category +component --list 156 components by category template --list page + block recipes docs color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography swizzle eject component source for deep customization diff --git a/package-lock.json b/package-lock.json index 2482140..bc78a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,11 @@ "@astrojs/markdoc": "2.0.6", "@astrojs/react": "6.0.2", "@astrojs/sitemap": "3.7.3", - "@astryxdesign/core": "0.3.0", - "@astryxdesign/theme-chocolate": "0.3.0", - "@astryxdesign/theme-matcha": "0.3.0", - "@astryxdesign/theme-neutral": "0.3.0", - "@astryxdesign/theme-stone": "0.3.0", + "@astryxdesign/core": "0.4.3", + "@astryxdesign/theme-chocolate": "0.4.3", + "@astryxdesign/theme-matcha": "0.4.3", + "@astryxdesign/theme-neutral": "0.4.3", + "@astryxdesign/theme-stone": "0.4.3", "@keystar/ui": "0.9.2", "@keystatic/astro": "5.2.0", "@keystatic/core": "0.6.4", @@ -26,7 +26,7 @@ }, "devDependencies": { "@astrojs/check": "0.9.6", - "@astryxdesign/cli": "0.3.0", + "@astryxdesign/cli": "0.4.3", "@types/node": "24.10.0", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", @@ -431,9 +431,9 @@ } }, "node_modules/@astryxdesign/cli": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.3.0.tgz", - "integrity": "sha512-iLFJsBlZUb66GskA1r3AmuodW72yu0PhCRAorcGPMuwHbGYn54pXGb/EnnWSdHf3Yuw4E3OuN7r4dY0TYJuYmQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.4.3.tgz", + "integrity": "sha512-U7DZnQYjr5+gjJOdGx+6cH4Wpdnky4u6WjfRpUs5W3kZ2IpBcoOI18GRFRZaEyxeQZRncAlrnNN+u6N9g6yJ5w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -480,9 +480,9 @@ } }, "node_modules/@astryxdesign/core": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.3.0.tgz", - "integrity": "sha512-t2hHvR7AcwW0oKrnxCwbdEEE5Ytgy0Ug61DcYAl9BxAuV5z4XL0/kMo5DiPvX9jbDxzysmOa8ClnXqzxouRUVQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.4.3.tgz", + "integrity": "sha512-NtOd1wPzTSmZPJ26dhau9cGJsFSGc1h+RRBqNhzTTHlf7d2heQQFXCsX1GdEuRp0BgCDtTcn2PUXhdtaHn3JvQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -495,54 +495,54 @@ } }, "node_modules/@astryxdesign/theme-chocolate": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/theme-chocolate/-/theme-chocolate-0.3.0.tgz", - "integrity": "sha512-Ji/qtJXkicwpSkrcHV4PDo9nHyeSZFHyEMTy10PDg+M20ZWZk9Bp7Q+Y5q/3fWg6MKKzdmxIQsxxxmWE0am7dQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-chocolate/-/theme-chocolate-0.4.3.tgz", + "integrity": "sha512-bcIjtf103X80Cj/p3DtevN+s8xpU4+TetFfDjAbd1runXn7Xp2MS8kWaMowHYwfdQvP11NWREmlVjYKJd/Mdcg==", "license": "MIT", "dependencies": { "lucide-react": "^1.18.0" }, "peerDependencies": { - "@astryxdesign/core": "0.3.0", + "@astryxdesign/core": "0.4.3", "react": ">=19" } }, "node_modules/@astryxdesign/theme-matcha": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/theme-matcha/-/theme-matcha-0.3.0.tgz", - "integrity": "sha512-9GBAhtrcCqaOCpJhkdaWN5vR2hd2u9JOKqx7JRfqzDPoDsEBi7gNJu1E+zccHZgGNA8OxFRTqpcgTLyluEtYvQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-matcha/-/theme-matcha-0.4.3.tgz", + "integrity": "sha512-Pf2HzMtX5dA/xr1CwDhzhLtv/xg0O1DEucJm84LCAN2YelJEF3HbCRI6u3YnetU7CYhv7LeC1pI2CU9LRVPkMw==", "license": "MIT", "dependencies": { "lucide-react": "^1.18.0" }, "peerDependencies": { - "@astryxdesign/core": "0.3.0", + "@astryxdesign/core": "0.4.3", "react": ">=19" } }, "node_modules/@astryxdesign/theme-neutral": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.3.0.tgz", - "integrity": "sha512-CeKRfmTEtww60WZK4pSlW7bheBNyVPpgEliiXXUHRStXgcXOhwiUA3D/cXhMJlEx+VRgVxnzl0hMbWIr42RaVQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.4.3.tgz", + "integrity": "sha512-V1qNRltEsOGcBz+riKgQOuOWQXlKOcfucOf3be8JdjCBjwH0xlWJSMmo5vfDQ2cf7L9XWskpUBmrCauUQorX4Q==", "license": "MIT", "dependencies": { "lucide-react": "^1.18.0" }, "peerDependencies": { - "@astryxdesign/core": "0.3.0", + "@astryxdesign/core": "0.4.3", "react": ">=19" } }, "node_modules/@astryxdesign/theme-stone": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/theme-stone/-/theme-stone-0.3.0.tgz", - "integrity": "sha512-GeSN8W4dU2rrTSRX0M93M9WbiNDQJ5dxV6UeOHI670gkx+drL08cIAtW7+HeQOpTLDjFG41qZWDTT2HxQ9pwNQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-stone/-/theme-stone-0.4.3.tgz", + "integrity": "sha512-s0Zih0O17mha1wL+8C3oPJ+R5kfwR7v9uIiVOIsXhr8tl++gHMHmKcWQcOL0NG1RNgi6KCvRDStapMn99e6Cbg==", "license": "MIT", "dependencies": { "lucide-react": "^1.18.0" }, "peerDependencies": { - "@astryxdesign/core": "0.3.0", + "@astryxdesign/core": "0.4.3", "react": ">=19" } }, diff --git a/package.json b/package.json index 895c828..7b16822 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,11 @@ "@astrojs/markdoc": "2.0.6", "@astrojs/react": "6.0.2", "@astrojs/sitemap": "3.7.3", - "@astryxdesign/core": "0.3.0", - "@astryxdesign/theme-chocolate": "0.3.0", - "@astryxdesign/theme-matcha": "0.3.0", - "@astryxdesign/theme-neutral": "0.3.0", - "@astryxdesign/theme-stone": "0.3.0", + "@astryxdesign/core": "0.4.3", + "@astryxdesign/theme-chocolate": "0.4.3", + "@astryxdesign/theme-matcha": "0.4.3", + "@astryxdesign/theme-neutral": "0.4.3", + "@astryxdesign/theme-stone": "0.4.3", "@keystar/ui": "0.9.2", "@keystatic/astro": "5.2.0", "@keystatic/core": "0.6.4", @@ -39,7 +39,7 @@ }, "devDependencies": { "@astrojs/check": "0.9.6", - "@astryxdesign/cli": "0.3.0", + "@astryxdesign/cli": "0.4.3", "@types/node": "24.10.0", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", From 00bc98121695ee41a2605c5e4fdae630a982cd2e Mon Sep 17 00:00:00 2001 From: walishcs Date: Mon, 17 Aug 2026 19:46:48 +0800 Subject: [PATCH 3/5] Publish bkuw blog post --- src/content/blog/bkuw-app.mdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/blog/bkuw-app.mdoc b/src/content/blog/bkuw-app.mdoc index 06b12db..2a777ab 100644 --- a/src/content/blog/bkuw-app.mdoc +++ b/src/content/blog/bkuw-app.mdoc @@ -3,7 +3,7 @@ title: bkuw — 語言田野調查工具箱 APP excerpt: | bkuw 是賽考利克泰雅語的「排列」之意,是一套為單一研究者設計的離線詞彙資料庫,希望讓詞彙從採集、整理到匯出,都能在同一個清楚而可靠的工作環境中完成。 publishedAt: 2026-08-17 -draft: true +draft: false tags: - bkuw - 田野調查 From 547e25867360952f63c541e3dda56ef47c303551 Mon Sep 17 00:00:00 2001 From: walishcs Date: Mon, 17 Aug 2026 19:48:14 +0800 Subject: [PATCH 4/5] fix: pass blog markdown as a string --- src/lib/blog-markdown.test.ts | 9 ++++----- src/pages/blog/[id].astro | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib/blog-markdown.test.ts b/src/lib/blog-markdown.test.ts index 2d5b3ff..5d64f7f 100644 --- a/src/lib/blog-markdown.test.ts +++ b/src/lib/blog-markdown.test.ts @@ -26,11 +26,10 @@ describe('prepareBlogMarkdown', () => { it('renders article structure with Astryx Markdown', () => { const html = renderToStaticMarkup( - createElement( - Markdown, - { headingLevelStart: 1 }, - '## Section\n\n- Bullet\n\n1. Numbered', - ), + createElement(Markdown, { + headingLevelStart: 1, + children: '## Section\n\n- Bullet\n\n1. Numbered', + }), ); expect(html).toContain(' - {body} - + children={body} + /> From 71f28f6ec6d8e313f99b89ce2862975875c28346 Mon Sep 17 00:00:00 2001 From: walishcs Date: Mon, 17 Aug 2026 19:50:18 +0800 Subject: [PATCH 5/5] fix: bridge Astro content to Astryx Markdown --- src/components/BlogMarkdown.tsx | 14 ++++++++++++++ src/lib/blog-markdown.test.ts | 7 +++---- src/pages/blog/[id].astro | 10 ++-------- 3 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 src/components/BlogMarkdown.tsx diff --git a/src/components/BlogMarkdown.tsx b/src/components/BlogMarkdown.tsx new file mode 100644 index 0000000..9b619dc --- /dev/null +++ b/src/components/BlogMarkdown.tsx @@ -0,0 +1,14 @@ +import { Markdown } from '@astryxdesign/core/Markdown'; + +export function BlogMarkdown({ body }: { body: string }) { + return ( + + {body} + + ); +} diff --git a/src/lib/blog-markdown.test.ts b/src/lib/blog-markdown.test.ts index 5d64f7f..599983c 100644 --- a/src/lib/blog-markdown.test.ts +++ b/src/lib/blog-markdown.test.ts @@ -1,7 +1,7 @@ -import { Markdown } from '@astryxdesign/core/Markdown'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; +import { BlogMarkdown } from '../components/BlogMarkdown'; import { prepareBlogMarkdown } from './blog-markdown'; describe('prepareBlogMarkdown', () => { @@ -26,9 +26,8 @@ describe('prepareBlogMarkdown', () => { it('renders article structure with Astryx Markdown', () => { const html = renderToStaticMarkup( - createElement(Markdown, { - headingLevelStart: 1, - children: '## Section\n\n- Bullet\n\n1. Numbered', + createElement(BlogMarkdown, { + body: '## Section\n\n- Bullet\n\n1. Numbered', }), ); diff --git a/src/pages/blog/[id].astro b/src/pages/blog/[id].astro index 6bd55a6..dd6038d 100644 --- a/src/pages/blog/[id].astro +++ b/src/pages/blog/[id].astro @@ -1,6 +1,6 @@ --- -import { Markdown } from '@astryxdesign/core/Markdown'; import type { CollectionEntry } from 'astro:content'; +import { BlogMarkdown } from '@/components/BlogMarkdown'; import BaseLayout from '@/layouts/BaseLayout.astro'; import { prepareBlogMarkdown } from '@/lib/blog-markdown'; import { getPublishedPosts } from '@/lib/collections'; @@ -58,12 +58,6 @@ const jsonLd = { ) } - +