Skip to content

Commit 9c273f2

Browse files
committed
feat: add multiple CMS improvements
- Add image compression to migration process - Replace svelte-select with bits-ui (better TypeScript support) - Fix slug input becoming date-only when cleared - Add per-user default author preference - Add slug redirect option when changing article URLs - Add unpublish confirmation warning - Add analytics page and dashboard widgets
1 parent 9a613db commit 9c273f2

File tree

121 files changed

+6972
-1925
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+6972
-1925
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,5 @@ run `bun tidy` after you finish your work. i.e. before commit
9797

9898
- Never use `as` or `any`. Let TypeScript infer types properly.
9999
- Never just "fire and forget". it crashes the entire server. instead, catch `.catch(console.error)` then forget, if you want to dispatch the job.
100+
101+
For detailed coding standards (import order, async patterns, naming conventions), see `docs/knowledges/coding-standards.md`.

bun.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"name": "cms.utcode.net",
77
"dependencies": {
88
"better-auth": "^1.4.6",
9+
"bits-ui": "^2.14.4",
910
"drizzle-valibot": "^0.4.2",
1011
"isomorphic-dompurify": "^2.34.0",
1112
"lucide-svelte": "^0.561.0",
@@ -148,6 +149,12 @@
148149

149150
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
150151

152+
"@floating-ui/core": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
153+
154+
"@floating-ui/dom": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
155+
156+
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
157+
151158
"@img/colour": ["@img/[email protected]", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
152159

153160
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
@@ -198,6 +205,8 @@
198205

199206
"@img/sharp-win32-x64": ["@img/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
200207

208+
"@internationalized/date": ["@internationalized/[email protected]", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="],
209+
201210
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
202211

203212
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -308,6 +317,8 @@
308317

309318
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/[email protected]", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
310319

320+
"@swc/helpers": ["@swc/[email protected]", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
321+
311322
"@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
312323

313324
"@tailwindcss/oxide": ["@tailwindcss/[email protected]", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
@@ -374,6 +385,8 @@
374385

375386
"bidi-js": ["[email protected]", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
376387

388+
"bits-ui": ["[email protected]", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="],
389+
377390
"block-stream2": ["[email protected]", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
378391

379392
"browser-or-node": ["[email protected]", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
@@ -420,6 +433,8 @@
420433

421434
"defu": ["[email protected]", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
422435

436+
"dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
437+
423438
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
424439

425440
"devalue": ["[email protected]", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
@@ -502,6 +517,8 @@
502517

503518
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
504519

520+
"inline-style-parser": ["[email protected]", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
521+
505522
"ipaddr.js": ["[email protected]", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
506523

507524
"is-arguments": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
@@ -570,6 +587,8 @@
570587

571588
"lucide-svelte": ["[email protected]", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-LxKemRvPNMNBjBa7JIVd4gZzBrTtwG1JA2ohpdM15jpQDcZCFWKyJz8eG1McaD0bq2weufkYSuVWQLclmmdUlw=="],
572589

590+
"lz-string": ["[email protected]", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
591+
573592
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
574593

575594
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
@@ -640,6 +659,8 @@
640659

641660
"rou3": ["[email protected]", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="],
642661

662+
"runed": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
663+
643664
"sade": ["[email protected]", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
644665

645666
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -680,14 +701,20 @@
680701

681702
"strnum": ["[email protected]", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
682703

704+
"style-to-object": ["[email protected]", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
705+
683706
"supports-preserve-symlinks-flag": ["[email protected]", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
684707

685708
"svelte": ["[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="],
686709

687710
"svelte-check": ["[email protected]", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw=="],
688711

712+
"svelte-toolbelt": ["[email protected]", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
713+
689714
"symbol-tree": ["[email protected]", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
690715

716+
"tabbable": ["[email protected]", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="],
717+
691718
"tailwindcss": ["[email protected]", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
692719

693720
"tapable": ["[email protected]", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Coding Standards
2+
3+
**Last Updated**: 2025-12-24
4+
5+
## Import Order
6+
7+
インポートは以下の順序で記述する:
8+
9+
1. **外部ライブラリ** - `@sveltejs/kit`, `valibot`, `drizzle-orm`
10+
2. **$app/*** - SvelteKitの内部API (`$app/server`, `$app/environment`等)
11+
3. **$lib/*** - プロジェクトの内部モジュール (階層順)
12+
- `$lib/server/database/*`
13+
- `$lib/server/services/*`
14+
- `$lib/server/drivers/*`
15+
- `$lib/shared/*`
16+
- `$lib/components/*`
17+
4. **型インポート** - `import type { ... }`は最後
18+
19+
###
20+
21+
```ts
22+
// ✅ 正しい順序
23+
import { error } from "@sveltejs/kit";
24+
import * as v from "valibot";
25+
import { command, query } from "$app/server";
26+
import { requireUtCodeMember } from "$lib/server/database/auth.server";
27+
import { createArticle } from "$lib/server/database/articles.server";
28+
import { purgeCache } from "$lib/server/services/cloudflare/cache.server";
29+
import type { Article } from "$lib/shared/models/types";
30+
31+
// ❌ 誤った順序
32+
import { createArticle } from "$lib/server/database/articles.server";
33+
import type { Article } from "$lib/shared/models/types";
34+
import { error } from "@sveltejs/kit";
35+
import { command, query } from "$app/server";
36+
```
37+
38+
## Async/Await パターン
39+
40+
### Fire-and-Forget パターン
41+
42+
非同期処理を「発射して忘れる」場合、必ず`.catch(console.error)`を使用する。
43+
44+
```ts
45+
// ✅ 正しいパターン
46+
purgeCache().catch(console.error);
47+
48+
db.update(article)
49+
.set({ viewCount: sql`${article.viewCount} + 1` })
50+
.where(eq(article.slug, slug))
51+
.catch(console.error);
52+
53+
// ❌ 誤ったパターン (サーバークラッシュの原因)
54+
purgeCache(); // エラーがキャッチされない
55+
56+
// ❌ 冗長なパターン
57+
purgeCache()
58+
.then(() => {})
59+
.catch(console.error);
60+
61+
// ❌ 古いパターン (Prettierが自動修正)
62+
purgeCache().then(() => {}, console.error);
63+
```
64+
65+
**理由**: サーバー環境では、catchされないPromiseリジェクションはサーバー全体をクラッシュさせる可能性がある。
66+
67+
### Async/Await の統一
68+
69+
非同期関数は一貫して`async/await`を使用する。`.then()`チェーンは避ける。
70+
71+
```ts
72+
// ✅ 正しいパターン
73+
export const getArticles = query(async () => {
74+
await requireUtCodeMember();
75+
return await listAllArticles();
76+
});
77+
78+
// ❌ 混在パターン (避ける)
79+
export const getArticles = query(() => {
80+
return requireUtCodeMember().then(() => listAllArticles());
81+
});
82+
```
83+
84+
## エラーハンドリング
85+
86+
### Remote Functions
87+
88+
Remote functionsでは、エラーは自動的にクライアントに伝播されるため、try-catchは不要。
89+
90+
```ts
91+
// ✅ シンプルなパターン
92+
export const deleteArticle = command(v.string(), async (id) => {
93+
const session = await requireUtCodeMember();
94+
await requireArticleOwnership(session, id);
95+
await deleteArticle(id);
96+
purgeCache().catch(console.error);
97+
});
98+
99+
// ❌ 不要なtry-catch
100+
export const deleteArticle = command(v.string(), async (id) => {
101+
try {
102+
const session = await requireUtCodeMember();
103+
await requireArticleOwnership(session, id);
104+
await deleteArticle(id);
105+
} catch (error) {
106+
throw error; // 不要
107+
}
108+
});
109+
```
110+
111+
例外: ビジネスロジックで特定のエラーを変換する場合のみtry-catchを使用。
112+
113+
### Database層
114+
115+
Database層では、エラーメッセージを明確にする。
116+
117+
```ts
118+
// ✅ 明確なエラーメッセージ
119+
export async function createArticle(data: NewArticle) {
120+
const [created] = await db.insert(article).values(data).returning();
121+
if (!created) throw new Error("Failed to create article");
122+
return created;
123+
}
124+
125+
// ❌ 曖昧なエラー
126+
export async function createArticle(data: NewArticle) {
127+
const [created] = await db.insert(article).values(data).returning();
128+
return created; // undefinedの可能性
129+
}
130+
```
131+
132+
## 命名規則
133+
134+
### ファイル命名
135+
136+
- **Remote Functions**: `*.remote.ts`
137+
- **Server Functions**: `*.server.ts`
138+
- **Shared Logic**: `*.ts` (サフィックスなし)
139+
- **Tests**: `*.test.ts`
140+
- **Types**: `types.ts` または `schema.ts`
141+
142+
### 関数命名
143+
144+
- **Remote Query**: 名詞形 (`getArticles`, `getMember`)
145+
- **Remote Command**: 動詞形 (`saveArticle`, `editMember`, `removeProject`)
146+
- **Server Functions**: 動詞形 (`listArticles`, `createMember`, `updateProject`)
147+
148+
```ts
149+
// Remote Functions (DAL)
150+
export const getArticles = query(async () => ...);
151+
export const saveArticle = command(..., async (data) => ...);
152+
153+
// Server Functions (DB)
154+
export async function listArticles() { ... }
155+
export async function createArticle(data: NewArticle) { ... }
156+
```
157+
158+
## Valibot バリデーション
159+
160+
### Picklist の使用
161+
162+
列挙型の値は`v.picklist()`を使用する。
163+
164+
```ts
165+
// ✅ 型安全なpicklist
166+
const PROJECT_CATEGORY_VALUES = [
167+
"active",
168+
"ended",
169+
"hackathon",
170+
"festival",
171+
"personal",
172+
] as const satisfies readonly ProjectCategory[];
173+
174+
const categorySchema = v.picklist(PROJECT_CATEGORY_VALUES);
175+
176+
// ❌ 型安全でないパターン
177+
const categorySchema = v.picklist(["active", "ended", "hackathon"]);
178+
```
179+
180+
## フォーマット
181+
182+
### インデント
183+
184+
- **タブ文字**を使用 (Prettier設定: `useTabs: true`)
185+
- スペースは使用しない
186+
187+
### 行の長さ
188+
189+
- 最大120文字 (Prettier設定: `printWidth: 120`)
190+
- 長いインポートや関数呼び出しは自動で折り返される
191+
192+
### セミコロン
193+
194+
- 常にセミコロンを使用 (Prettier設定: `semi: true`)
195+
196+
### クォート
197+
198+
- ダブルクォートを使用 (Prettier設定: `useTabs: true`)
199+
200+
## ツール
201+
202+
### 自動フォーマット
203+
204+
```sh
205+
bun tidy # type-check + test-check + biome + prettier
206+
bun fix # biome + prettier のみ
207+
```
208+
209+
### 個別チェック
210+
211+
```sh
212+
bun type-check # TypeScript型チェック
213+
bun test-check # 単体テスト
214+
bun lint-check # Biomeリント
215+
bun format-check # Prettierフォーマット
216+
```
217+
218+
## 検証ルール
219+
220+
コミット前に必ず以下を実行:
221+
222+
1. `bun type-check` - エラー0であること
223+
2. `bun tidy` - エラー・警告0であること
224+
225+
## 参考
226+
227+
- CLAUDE.md - プロジェクト固有のワークフロー
228+
- security.md - セキュリティ設計
229+
- project-context.md - プロジェクト概要

0 commit comments

Comments
 (0)