diff --git a/.agent/skills/git-manager/SKILL.md b/.agent/skills/git-manager/SKILL.md index a886e83..633892c 100644 --- a/.agent/skills/git-manager/SKILL.md +++ b/.agent/skills/git-manager/SKILL.md @@ -21,7 +21,7 @@ Follow the **Conventional Commits** format: - `fix`: Bug fixes (hydration error, layout shift) - `docs`: Documentation changes - `style`: Code changes without logic change -- `refactor`: Code changes without logic change +- `refactor`: Code changes that improve code structure - `test`: Test changes - `chore`: Config changes, dependency updates diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md deleted file mode 100644 index 6eca5c4..0000000 --- a/.gemini/GEMINI.md +++ /dev/null @@ -1,87 +0,0 @@ -# Project Guidelines & Rules - -## 1. Project Overview & Tech Stack -- **Description:** A modern blog application using a Notion database as a Headless CMS, built with Next.js. -- **Framework:** Next.js 16.1.1 (App Router) -- **UI Library:** React 19 -- **Language:** TypeScript (Strict Mode) -- **Styling:** Tailwind CSS 4, `class-variance-authority`, `clsx`, `lucide-react`. (**Do not create .css files**; use utility classes). -- **CMS/Database:** Notion API (`@notionhq/client`) serving as the primary data source. -- **Testing:** Vitest (Unit), Playwright (E2E), Testing Library. -- **Package Manager:** `pnpm` - -## 2. AI Behavior Guidelines -- **Language:** Always provide explanations and responses in **Korean**. However, write code comments, variable names, and commit messages in **English**. -- **Reasoning:** Before writing code, always output the **"Reasoning Process"**. If the modification is extensive, present a **"Plan"** first and await user approval. -- **Code Quality:** Prefer clear, multi-line code over unreadable one-liners. Remove unused variables and imports immediately. -- **Error Handling:** When an error occurs, do not just fix it; summarize the **root cause** in one line. If a terminal command fails, stop the process immediately. -- **Documentation:** Every works must be documented in /docs folder. There are several types of documents: - - **/architectures** (for used architecture of the project) - - **/fixes** (for fixing bugs) - - **/plans** (for planning features) - - **/tests** (for testing features) - - ... (if you need to add more documents, add them and save them in /docs folder) - - **CHANGELOG.md** (always update this file when you make a change) -- **Workflow Automation Rule:** - - When a task is deemed complete or the user indicates completion, do not commit immediately; **always initiate the `@finish` workflow.** - - Enforce the **[Code Review] -> [Request Approval] -> [Commit]** procedure. Do not suggest `git commit` directly. - -## 3. Server vs. Client Strategy (Architecture) -- **Default to Server:** All components are **Server Components** by default. -- **Client Boundary:** Use `'use client'` only when interactivity (`useState`, `onClick`, `useEffect`) is strictly needed. -- **Data Fetching:** - Fetch data directly in Server Components using `async/await`. - - **Do not** use `useEffect` for data fetching unless absolutely necessary. - - **Caching:** Notion API calls must be cached for **6 hours (21600s)** using `unstable_cache` to minimize API usage. -- **Mutations:** Use **Server Actions** for any data mutations or form submissions. Do not use `pages/api` (API Routes). - -## 4. Coding Standards - -### 4.1 Naming & Structure -- **Components:** `PascalCase` (e.g., `PostCard.tsx`). -- **Functions/Variables:** `camelCase` (e.g., `getPostData`). -- **Files:** Match the export name or use `kebab-case` for utility files/folders (e.g., `app/dashboard/settings`). -- **Imports:** Use **absolute imports** (`@/components/...`) instead of relative paths. -- **Co-location:** Group related components in subdirectories (e.g., `src/components/notion/`). - -### 4.2 Type Safety -- **No `any`:** The use of `any` is strictly prohibited. Use `unknown` with Type Guards (or Zod) for validation. -- **Strict Mode:** Assume `strict: true`; handle `null` or `undefined` rigorously. -- **Explicit Returns:** Explicitly define the return type for all functions (do not rely on inference). -- **Definitions:** Use `interface` for extensible objects; use `type` for unions/intersections. - -### 4.3 State Management Hierarchy -1. **Server State:** Prefer fetching fresh data on the server (React Query/SWR if client-side fetching is needed). -2. **URL State:** Store filter/search params in the URL (`useSearchParams`) for shareability. -3. **Global State:** Use Context API or Zustand only if prop drilling exceeds 3 levels. - -### 4.4 Error Handling Pattern -- **Server Actions:** Handle errors gracefully using `try/catch` and return structured objects: - ```ts - try { - // logic - } catch (error) { - if (error instanceof Error) { - console.error(error.message); - } - return { success: false, error: "Friendly error message" }; - } - ``` - -## 5. Workflows to AVOID ๐Ÿšซ -- No API Routes: Do not use pages/api; use Server Actions. -- No Hardcoded Secrets: Always use process.env. -- No Huge Components: Break down components if they exceed 200 lines. -- No New Packages: Do not install new npm packages without asking for approval first. -- No Prop Drilling: Avoid passing props more than 3 levels deep. - -## 6. Environment & Commands -- Environment Variables: NOTION_API_KEY, NOTION_POSTS_DATA_SOURCE_ID, NOTION_COMMENTS_DATA_SOURCE_ID. -- Commands: - - pnpm dev: Start dev server (http://localhost:3000). - - pnpm build: Production build. - - pnpm test: Run unit tests. - - pnpm test:e2e: Run E2E tests. - -## 7. Documentation Etiquette -- Code Comments: When writing complex logic, write a brief comment explaining "Why" (intent), not "What" (syntax). -- JSDoc: Mandatory for all utility functions. diff --git a/.gemini/skills/SKILL.example.md b/.gemini/skills/SKILL.example.md deleted file mode 100644 index b726e97..0000000 --- a/.gemini/skills/SKILL.example.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: [์Šคํ‚ฌ ์ด๋ฆ„] -description: [์ด ์Šคํ‚ฌ์ด ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ํ•œ ์ค„ ์š”์•ฝ] -triggers: - - [ํŠธ๋ฆฌ๊ฑฐ ํ‚ค์›Œ๋“œ 1] (์˜ˆ: @notion, @db) - - [ํŠธ๋ฆฌ๊ฑฐ ํ‚ค์›Œ๋“œ 2] ---- - -# [Skill Name] Context -์ด ํŒŒ์ผ์€ [ํŠน์ • ์ž‘์—…]์„ ์ˆ˜ํ–‰ํ•  ๋•Œ AI๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•  ๊ฐ€์ด๋“œ๋ผ์ธ์ž…๋‹ˆ๋‹ค. - -## 1. Goal (๋ชฉํ‘œ) -- ๋ฌด์—‡์„ ๋‹ฌ์„ฑํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ์ •์˜ - -## 2. Rules (์ œ์•ฝ ์‚ฌํ•ญ) -- ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค - -## 3. Tool Usage (๋„๊ตฌ ์‚ฌ์šฉ๋ฒ•) -- ๊ด€๋ จ๋œ ํ•จ์ˆ˜๋‚˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๋ฐฉ๋ฒ• - -## 4. Examples (์˜ˆ์‹œ - ๊ฐ€์žฅ ์ค‘์š” โญ) -User: [์งˆ๋ฌธ ์˜ˆ์‹œ] -AI: [์ด์ƒ์ ์ธ ๋‹ต๋ณ€ ์˜ˆ์‹œ] diff --git a/.gemini/skills/code-review/SKILL.md b/.gemini/skills/code-review/SKILL.md deleted file mode 100644 index 7cffc1b..0000000 --- a/.gemini/skills/code-review/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: Senior Code Reviewer -description: Analyze code for performance, security, type safety, and Next.js best practices. -triggers: - - @review - - @cr - - "์ฝ”๋“œ ๋ฆฌ๋ทฐํ•ด์ค˜" - - "์ด ์ฝ”๋“œ ๋ฌธ์ œ์  ์žˆ์–ด?" ---- - -# Code Review Skill - -## 1. Context -You are a Senior Full-Stack Engineer reviewing a Pull Request. Your goal is to catch logical errors, performance bottlenecks, and maintainability issues before they merge. - -## 2. Review Checklist (Priority Order) -1. **Server vs Client Boundary:** - - Is `'use client'` used only when necessary? (Interactivity, Hooks) - - Are sensitive environment variables (`process.env.NOTION_API_KEY`) leaking into Client Components? -2. **Performance (Critical):** - - Are Notion API calls wrapped in `unstable_cache`? (Must be cached for 6 hours/21600s) - - Are images using `next/image` with proper `sizes` and `alt` tags? -3. **Type Safety:** - - Are there any explicit `any` types? (Flag immediately as Critical) - - Are API responses validated (e.g., checking if `properties` exist in Notion response)? -4. **Code Style:** - - Is the component too large (>200 lines)? Suggest splitting. - - Are tailwind classes organized? (Suggest `clsx` or `cn` usage for conditionals). - -## 3. Output Format -Provide feedback in the following format: - -- **๐Ÿ”ด Critical:** (Bugs, Security, `any` type, Infinite loops) -- **๐ŸŸก Improvement:** (Performance optimizations, better readability) -- **๐ŸŸข Good:** (Praise for good patterns) - -## 4. Example Interaction -**User:** "Review this code: `const data = await fetch(url).then(res => res.json())`" -**AI:** -"**๐Ÿ”ด Critical:** -1. **Promise Handling:** Use `async/await` instead of `.then()` chains for readability. -2. **Type Safety:** The return type is implicit `any`. Define an interface for the response. -3. **Error Handling:** There is no `try/catch` or error checking. - -**๐ŸŸก Improvement:** -- If this is a Server Component, consider wrapping `fetch` with `unstable_cache` if the data doesn't change often." - \ No newline at end of file diff --git a/.gemini/skills/git-manager/SKILL.md b/.gemini/skills/git-manager/SKILL.md deleted file mode 100644 index a886e83..0000000 --- a/.gemini/skills/git-manager/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Git Commit Manager -description: Generate semantic commit messages and manage branches. -triggers: - - @git - - @wt - - @cm - - "commit message" - - "์ปค๋ฐ‹ํ•ด์ค˜" ---- - -# Git Manager Skill - -## 1. Context & Resources -- **Role:** You are an expert Git manager utilizing **Worktrees** for parallel development. -- **Reference:** For specific commands (especially Worktree & Bare Repo setup), **ALWAYS refer to `./cheatsheet.md`** located in the same directory. - -## 2. Commit Message Convention -Follow the **Conventional Commits** format: -- `feat`: New features (blog post rendering, new API route) -- `fix`: Bug fixes (hydration error, layout shift) -- `docs`: Documentation changes -- `style`: Code changes without logic change -- `refactor`: Code changes without logic change -- `test`: Test changes -- `chore`: Config changes, dependency updates - -## 3. Workflow -1. Analyze the `git diff` output. -2. Summarize changes in English (imperative mood). -3. If the change is huge, suggest splitting the commit. -4. Consult `./cheatsheet.md` for exact syntax. - -## 4. Example -**Input:** Changed the header background color and added a logo. -**Output:** -```bash -style: update header design with new logo - -- Change background color to neutral-900 -- Add Logo component to navigation -``` \ No newline at end of file diff --git a/.gemini/skills/git-manager/cheatsheet.md b/.gemini/skills/git-manager/cheatsheet.md deleted file mode 100644 index de59287..0000000 --- a/.gemini/skills/git-manager/cheatsheet.md +++ /dev/null @@ -1,81 +0,0 @@ -# ๋ช…๋ น์–ด,์„ค๋ช… - -```bash -git config --global user.name "์ด๋ฆ„" # ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์„ค์ • -git config --global user.email "์ด๋ฉ”์ผ" # ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ ์„ค์ • -git init # ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ Git ์ €์žฅ์†Œ๋กœ ์ดˆ๊ธฐํ™” -git clone --bare .bare # ์›ŒํฌํŠธ๋ฆฌ ์‚ฌ์šฉ์„ ์œ„ํ•œ Bare Clone -``` - -## ์›ŒํฌํŠธ๋ฆฌ -์›ŒํฌํŠธ๋ฆฌ ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋ช…๋ น์–ด -root ํด๋”์— .bare ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ทธ ์•ˆ์— ์›ŒํฌํŠธ๋ฆฌ ํด๋”๋ฅผ ์ƒ์„ฑ -**ํ•ญ์ƒ root ํด๋”์—์„œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•จ** - -```bash - -git -C .bare worktree list # ํ˜„์žฌ ์ƒ์„ฑ๋œ ๋ชจ๋“  ์›ŒํฌํŠธ๋ฆฌ(์ž‘์—… ํด๋”) ๋ชฉ๋ก ํ™•์ธ -git -C .bare worktree add ../<ํด๋”๋ช…> <๋ธŒ๋žœ์น˜๋ช…> # ์ƒˆ ๋ธŒ๋žœ์น˜๋ฅผ ๋”ฐ๋ฉด์„œ ์ƒˆ ํด๋” ์ƒ์„ฑ -git -C .bare worktree add ../<ํด๋”๋ช…> # ๊ธฐ์กด ๋ธŒ๋žœ์น˜๋ฅผ ์ƒˆ ํด๋”๋กœ ์ฒดํฌ์•„์›ƒ -git -C .bare worktree remove <ํด๋”๋ช…> # ์ž‘์—… ํด๋” ์‚ญ์ œ (Git ์—ฐ๊ฒฐ ํ•ด์ œ) -git -C .bare worktree prune # ํด๋”๋ฅผ ๊ฐ•์ œ ์‚ญ์ œ(rm -rf)ํ–ˆ์„ ๋•Œ ์ฐŒ๊บผ๊ธฐ ์ •๋ฆฌ -git -C .bare worktree move <๊ตฌํด๋”> ../<์‹ ํด๋”> # ์›ŒํฌํŠธ๋ฆฌ ํด๋” ๊ฒฝ๋กœ/์ด๋ฆ„ ๋ณ€๊ฒฝ -``` - -## ์ปค๋ฐ‹ - -```bash -git status # ํ˜„์žฌ ํŒŒ์ผ ์ƒํƒœ(๋ณ€๊ฒฝ๋จ, ์Šคํ…Œ์ด์ง•๋จ) ํ™•์ธ -git add . # ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์Šคํ…Œ์ด์ง• ์˜์—ญ(Staging Area)์— ์ถ”๊ฐ€ -git add <ํŒŒ์ผ> # ํŠน์ • ํŒŒ์ผ๋งŒ ์Šคํ…Œ์ด์ง• -git commit -m "๋ฉ”์‹œ์ง€" # ์Šคํ…Œ์ด์ง•๋œ ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ •(์ €์žฅ) -git commit --amend # ๋ฐฉ๊ธˆ ํ•œ ์ปค๋ฐ‹์˜ ๋ฉ”์‹œ์ง€๋‚˜ ํŒŒ์ผ ์ˆ˜์ • (๋ฎ์–ด์“ฐ๊ธฐ) -``` - -## ๋ธŒ๋žœ์น˜ - -```bash -git branch # ๋กœ์ปฌ ๋ธŒ๋žœ์น˜ ๋ชฉ๋ก ํ™•์ธ -git branch -r # ์›๊ฒฉ ๋ธŒ๋žœ์น˜ ๋ชฉ๋ก ํ™•์ธ -git branch <์ด๋ฆ„> # ์ƒˆ ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ (์ด๋™์€ ์•ˆ ํ•จ) -git branch -m <์ƒˆ์ด๋ฆ„> # ํ˜„์žฌ ๋ธŒ๋žœ์น˜ ์ด๋ฆ„ ๋ณ€๊ฒฝ -git branch -d <์ด๋ฆ„> # ๋ธŒ๋žœ์น˜ ์‚ญ์ œ (๋ณ‘ํ•ฉ๋œ ๊ฒƒ๋งŒ) -git branch -D <์ด๋ฆ„> # ๋ธŒ๋žœ์น˜ ๊ฐ•์ œ ์‚ญ์ œ -git switch <๋ธŒ๋žœ์น˜> # ํ•ด๋‹น ๋ธŒ๋žœ์น˜๋กœ ์ด๋™ -git switch -c <์ด๋ฆ„> # ์ƒˆ ๋ธŒ๋žœ์น˜ ๋งŒ๋“ค๋ฉด์„œ ์ด๋™ -``` - -## ์›๊ฒฉ ์ €์žฅ์†Œ - -```bash -git fetch # ์›๊ฒฉ ์ €์žฅ์†Œ์˜ ์ตœ์‹  ์ด๋ ฅ๋งŒ ๊ฐ€์ ธ์˜ด (๋ณ‘ํ•ฉ X) -git pull origin <๋ธŒ๋žœ์น˜> # ์›๊ฒฉ ๋‚ด์šฉ์„ ๊ฐ€์ ธ์™€์„œ ํ•ฉ์นจ (Fetch + Merge) -git push origin <๋ธŒ๋žœ์น˜> # ๋‚ด ์ปค๋ฐ‹์„ ์›๊ฒฉ ์ €์žฅ์†Œ์— ์˜ฌ๋ฆผ -git push -u origin <๋ธŒ๋žœ์น˜> # ์—…์ŠคํŠธ๋ฆผ ์„ค์ • (๋‹ค์Œ๋ถ€ํ„ด git push๋งŒ ํ•ด๋„ ๋จ) -git merge <๋ธŒ๋žœ์น˜> # ๋‹ค๋ฅธ ๋ธŒ๋žœ์น˜๋ฅผ ํ˜„์žฌ ๋ธŒ๋žœ์น˜๋กœ ํ•ฉ์นจ -git rebase <๋ธŒ๋žœ์น˜> # ๋‚ด ๋ธŒ๋žœ์น˜์˜ ์‹œ์ž‘์ ์„ ํƒ€๊ฒŸ ๋ธŒ๋žœ์น˜ ๋์œผ๋กœ ์˜ฎ๊น€ (๊น”๋”ํ•œ ํžˆ์Šคํ† ๋ฆฌ) -``` - -## ๋ณต๊ตฌ - -```bash -git restore <ํŒŒ์ผ> # ์ž‘์—… ์ค‘์ธ ํŒŒ์ผ ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ทจ์†Œ (๋งˆ์ง€๋ง‰ ์ปค๋ฐ‹ ์ƒํƒœ๋กœ) -git restore --staged <ํŒŒ์ผ> # git add ์ทจ์†Œ (์Šคํ…Œ์ด์ง• ๋‚ด๋ฆฌ๊ธฐ) -git reset --soft HEAD~1 # ์ปค๋ฐ‹์€ ์ทจ์†Œํ•˜๋˜, ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ์Šคํ…Œ์ด์ง• ์ƒํƒœ๋กœ ๋ณด์กด -git reset --hard HEAD~1 # ์ปค๋ฐ‹๊ณผ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋ชจ๋‘ ๋‚ ๋ ค๋ฒ„๋ฆผ (๋ณต๊ตฌ ๋ถˆ๊ฐ€) -git revert <์ปค๋ฐ‹ID> # ํŠน์ • ์ปค๋ฐ‹์˜ ๋‚ด์šฉ์„ ๋ฐ˜๋Œ€๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ์ƒˆ ์ปค๋ฐ‹ ์ƒ์„ฑ (ํ˜‘์—… ์‹œ ์•ˆ์ „) -``` - -## ์ž„์‹œ ์ €์žฅ -์ž„์‹œ ์ €์žฅ ํ•˜์ง€ ์•Š์Œ. ์›ŒํฌํŠธ๋ฆฌ ์‚ฌ์šฉ. - -## ํžˆ์Šคํ† ๋ฆฌ - -```bash -git log # ์ปค๋ฐ‹ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ -git log --oneline --graph # ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๊ทธ๋ž˜ํ”„ ํ˜•ํƒœ๋กœ ํ•œ ์ค„ ์š”์•ฝํ•ด์„œ ๋ณด๊ธฐ -git diff # ์Šคํ…Œ์ด์ง•๋˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ -git show <์ปค๋ฐ‹ID> # ํŠน์ • ์ปค๋ฐ‹์˜ ์ƒ์„ธ ๋ณ€๊ฒฝ ๋‚ด์šฉ ํ™•์ธ -git blame <ํŒŒ์ผ> # "ํŒŒ์ผ์˜ ๊ฐ ๋ผ์ธ์„ ๋ˆ„๊ฐ€, ์–ธ์ œ ์ˆ˜์ •ํ–ˆ๋Š”์ง€ ๋ฒ”์ธ(?) ์ฐพ๊ธฐ" -git bisect # ์ด์ง„ ํƒ์ƒ‰์œผ๋กœ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ปค๋ฐ‹ ์ฐพ์•„๋‚ด๊ธฐ -``` diff --git a/.gemini/skills/notion-cms/SKILL.md b/.gemini/skills/notion-cms/SKILL.md deleted file mode 100644 index 4f04ece..0000000 --- a/.gemini/skills/notion-cms/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Notion CMS Handler -description: Manage Notion API interactions, database queries, and data fetching logic. -triggers: - - @notion - - @cms - - "fetch post" - - "๋…ธ์…˜" ---- - -# Notion CMS Skill - -## 1. Context -You are a Notion API expert for a Next.js blog. You handle data fetching from the Notion Database ID provided in `.env`. - -## 2. Critical Rules -1. **Caching:** Must use `unstable_cache` for all `notion.databases.query` calls. Revalidate time is 21600s. -2. **SDK:** Use `@notionhq/client`. -3. **Transformer:** Always transform the raw Notion response into a simplified `Post` interface before returning it to the component. Do not leak raw Notion blocks to the UI. -4. **Filter:** Only fetch posts where `status` is 'Published'. - -## 3. Code Pattern (Copy this style) -```typescript -import { Client } from "@notionhq/client"; -import { unstable_cache } from "next/cache"; - -const notion = new Client({ auth: process.env.NOTION_API_KEY }); - -export const getPosts = unstable_cache( - async () => { - // ...implementation... - }, - ["posts"], - { revalidate: 21600 } -); -``` diff --git a/.gemini/skills/test-engineer/SKILL.md b/.gemini/skills/test-engineer/SKILL.md deleted file mode 100644 index 06516a7..0000000 --- a/.gemini/skills/test-engineer/SKILL.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Test Engineer -description: Write and debug Unit (Vitest) and E2E (Playwright) tests. -triggers: - - @test - - "ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•ด์ค˜" - - "์—๋Ÿฌ ๊ณ ์ณ์ค˜" ---- - -# Test Engineer Skill - -## 1. Role -You are a QA Engineer specialized in Vitest and Playwright. - -## 2. File Placement Rules -- **Unit Tests:** Place in `__tests__/unit/{filename}.test.tsx` -- **E2E Tests:** Place in `__tests__/e2e/{feature}.spec.ts` - -## 3. Writing Strategy -- **Unit:** Do not test implementation details. Test behaviors (inputs/outputs). Use `screen.getByRole` for accessibility compliance. -- **E2E:** Always capture screenshots on failure (`screenshot: 'only-on-failure'`). Mock external API calls (Notion) using `page.route` to avoid hitting real limits. -- **CI/CD:** Ensure `pnpm test` and `pnpm test:e2e` pass before committing. - -## 4. Command Reference -- Run Unit: `pnpm test` -- Run E2E: `pnpm test:e2e` -- Debug Mode: `pnpm test:ui` - -## 5. Example Interaction -User: "Create a test for the Comment component." -AI: "Created `__tests__/unit/Comment.test.tsx`. I mocked the submission API to prevent network requests. Would you like me to run `pnpm test` now?" diff --git a/.gemini/skills/ui/SKILL.md b/.gemini/skills/ui/SKILL.md deleted file mode 100644 index 7bca4a0..0000000 --- a/.gemini/skills/ui/SKILL.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: UI/UX Designer -description: Design accessible, responsive, and aesthetic components using Tailwind CSS v4. -triggers: - - @ui - - @design - - "๋””์ž์ธํ•ด์ค˜" - - "์˜ˆ์˜๊ฒŒ ๋งŒ๋“ค์–ด์ค˜" ---- - -# UI/UX Design Skill - -## 1. Context -You are a Product Designer specialized in Tailwind CSS v4 and Headless UI systems. You prioritize "Mobile First", "Dark Mode", and "Accessibility (a11y)". - -## 2. Design Principles -- **Mobile First:** Always write base classes for mobile, then `md:`, `lg:` for larger screens. -- **Dark Mode Support:** Every color class must have a `dark:` counterpart. (e.g., `bg-white dark:bg-neutral-950`). -- **Interaction:** Add states for `hover:`, `focus-visible:` (for keyboard nav), and `active:`. -- **Spacing:** Use consistent spacing (multiples of 4). `p-4`, `gap-6`, `my-8`. - -## 3. Tech Constraints -- **Library:** Use `lucide-react` for icons. -- **Utils:** Use `cn` (from `lib/utils`) for class merging. -- **Tailwind v4:** Do not use `config` based arbitrary values if possible. Use standard utility classes. diff --git a/.gemini/skills/workflows/SKILL.md b/.gemini/skills/workflows/SKILL.md deleted file mode 100644 index feb6bc2..0000000 --- a/.gemini/skills/workflows/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: Task Finisher Workflow -description: Automate the Code Review -> Confirmation -> Git Commit pipeline. -triggers: - - @finish - - @done - - "์ž‘์—… ์™„๋ฃŒ" - - "๋งˆ๋ฌด๋ฆฌํ•ด์ค˜" ---- - -# Task Finisher Workflow - -## 1. Context -You are the **Workflow Orchestrator**. Your goal is to ensure high code quality before finalizing any changes. You act as a bridge between the `@review` skill and the `@git` skill. - -## 2. Execution Steps (Follow Strictly) - -### Step 1: Pre-Commit Review (Invoke Code-Reviewer) -1. Read all modified files (`git diff`). -2. **Act as the `@review` agent:** - - Check for `any` types, missing `unstable_cache`, and logic errors. - - Check if strictly necessary `test` files exist (if logic was changed). -3. **Report:** - - If **Critical Issues** are found ๐Ÿ”ด -> **STOP** and ask the user to fix them. - - If only **Minor Issues** or **Good** ๐ŸŸข -> Present the summary and ask: - > "๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ๊ฐ€ ์–‘ํ˜ธํ•ฉ๋‹ˆ๋‹ค. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ง„ํ–‰ํ• ๊นŒ์š”?" - -### Step 2: User Confirmation (Wait) -- **DO NOT** generate the commit command yet. -- Wait for the user to reply "Yes", "Proceed", or "ใ…‡ใ…‡". - -### Step 3: Commit Generation (Invoke Git-Manager) -1. Once confirmed, **Act as the `@git` agent:** - - Generate a specific `git commit` command following Conventional Commits. - - Provide the command for the user to run (or run it if permissions allow). - -## 3. Example Interaction - -**User:** `@finish` (Auth login implementation) - -**AI:** -"**๐Ÿ”Ž Code Review Summary:** -- ๐ŸŸข **Security:** `process.env` is properly used. -- ๐ŸŸก **Suggestion:** Consider extracting the validation logic to `lib/auth.ts`. -- โœ… **Tests:** Unit tests passed. - -์ฝ”๋“œ๊ฐ€ ์•ˆ์ •์ ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ปค๋ฐ‹์„ ์ง„ํ–‰ํ• ๊นŒ์š”?" - -**User:** "์‘ ์ง„ํ–‰ํ•ด" - -**AI:** -"**๐Ÿš€ Git Commit:** -```bash -git add . -git commit -m "feat: implement auth login with validation" diff --git a/__tests__/unit/components/notion/BlockRenderer.test.tsx b/__tests__/unit/components/notion/BlockRenderer.test.tsx index 647ef94..79a856b 100644 --- a/__tests__/unit/components/notion/BlockRenderer.test.tsx +++ b/__tests__/unit/components/notion/BlockRenderer.test.tsx @@ -4,6 +4,15 @@ import { BlockRenderer } from '@/components/notion/BlockRenderer'; import { createTestBlock } from '../../../helpers/block-test-helpers'; // Mock next/image to avoid width/height requirement in tests +// Mock react-syntax-highlighter +vi.mock('react-syntax-highlighter', () => ({ + Prism: ({ children }: { children: string }) =>
{children}
, +})); + +vi.mock('react-syntax-highlighter/dist/esm/styles/prism', () => ({ + atomDark: {}, +})); + vi.mock('next/image', () => ({ default: ({ src, alt, className }: { src: string; alt: string; className?: string }) => ( // eslint-disable-next-line @next/next/no-img-element diff --git a/next.config.ts b/next.config.ts index c66fbb7..cf83429 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,7 +11,8 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ const nextConfig: NextConfig = { images: { - formats: ['image/webp', 'image/avif'], + deviceSizes: [640, 1080, 1920], + imageSizes: [64, 128, 256], remotePatterns: [ { protocol: "https", @@ -26,8 +27,6 @@ const nextConfig: NextConfig = { hostname: "**.amazonaws.com", }, ], - deviceSizes: [640, 750, 828, 1080, 1200, 1920], - imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, // async redirects() { // return [ diff --git a/package.json b/package.json index d8f2a09..927ba72 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", + "react-syntax-highlighter": "^16.1.0", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -36,6 +37,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.1.2", "@vitest/coverage-v8": "4.0.16", "dotenv": "^17.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fb897b..4e34f6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) + react-syntax-highlighter: + specifier: ^16.1.0 + version: 16.1.0(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -72,6 +75,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.7) + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 '@vitejs/plugin-react': specifier: ^5.1.2 version: 5.1.2(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)) @@ -1157,6 +1163,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1166,14 +1175,26 @@ packages: '@types/node@20.19.27': resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 + '@types/react-syntax-highlighter@15.5.13': + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.51.0': resolution: {integrity: sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1574,6 +1595,15 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1591,6 +1621,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -1658,6 +1691,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1901,6 +1937,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1933,6 +1972,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2034,12 +2077,24 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2078,6 +2133,12 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2113,6 +2174,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2129,6 +2193,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2359,6 +2426,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -2539,6 +2609,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -2600,9 +2673,16 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2625,6 +2705,12 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react-syntax-highlighter@16.1.0: + resolution: {integrity: sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==} + engines: {node: '>= 16.20.2'} + peerDependencies: + react: '>= 0.14.0' + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -2633,6 +2719,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + refractor@5.0.0: + resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -2748,6 +2837,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3962,6 +4054,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -3970,14 +4066,24 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/prismjs@1.26.5': {} + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 + '@types/react-syntax-highlighter@15.5.13': + dependencies: + '@types/react': 19.2.7 + '@types/react@19.2.7': dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4385,6 +4491,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -4399,6 +4511,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@7.2.0: {} concat-map@0.0.1: {} @@ -4462,6 +4576,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -4872,6 +4990,10 @@ snapshots: dependencies: reusify: 1.1.0 + fault@1.0.4: + dependencies: + format: 0.2.2 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -4900,6 +5022,8 @@ snapshots: dependencies: is-callable: 1.2.7 + format@0.2.2: {} + fsevents@2.3.2: optional: true @@ -4998,12 +5122,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.7.0 @@ -5050,6 +5190,13 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -5094,6 +5241,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -5112,6 +5261,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -5333,6 +5484,11 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + lru-cache@11.2.4: {} lru-cache@5.1.1: @@ -5517,6 +5673,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -5567,12 +5733,16 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + prismjs@1.30.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -5588,6 +5758,16 @@ snapshots: react-refresh@0.18.0: {} + react-syntax-highlighter@16.1.0(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 19.2.3 + refractor: 5.0.0 + react@19.2.3: {} reflect.getprototypeof@1.0.10: @@ -5601,6 +5781,13 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + refractor@5.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/prismjs': 1.26.5 + hastscript: 9.0.1 + parse-entities: 4.0.2 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -5789,6 +5976,8 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stable-hash@0.0.5: {} stackback@0.0.2: {} diff --git a/src/app/[locale]/[slug]/page.tsx b/src/app/[locale]/[slug]/page.tsx index a079c1c..5bd435d 100644 --- a/src/app/[locale]/[slug]/page.tsx +++ b/src/app/[locale]/[slug]/page.tsx @@ -1,9 +1,11 @@ -import { getPageContent, getPostBySlug, getPublishedPosts, getPostById } from "@/lib/services/posts.service"; -import { BlockRenderer } from "@/components/notion/BlockRenderer"; -import { Link } from "@/i18n/routing"; import Image from "next/image"; import { notFound } from "next/navigation"; import { getTranslations } from 'next-intl/server'; +import { Metadata } from "next"; + +import { getPageContent, getPostBySlug, getPublishedPosts, getPostById } from "@/lib/services/posts.service"; +import { BlockRenderer } from "@/components/notion/BlockRenderer"; +import { Link } from "@/i18n/routing"; import { LanguageToggle } from "@/components/utils/LanguageToggle"; import { ViewTracker } from "@/components/utils/ViewTracker"; import { PostEngagement } from "@/components/posts/PostEngagement"; @@ -11,12 +13,6 @@ import { CommentSection } from "@/components/comments/CommentSection"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import { CommentErrorFallback } from "@/components/error-fallbacks/CommentErrorFallback"; -export const dynamic = 'force-dynamic'; - -export const revalidate = 3600; // Revalidate every 1 hour - -import { Metadata } from "next"; - export async function generateStaticParams() { const posts = await getPublishedPosts(); if (!posts) return []; @@ -38,6 +34,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str return { title: post.title, description: post.description || `Read ${post.title} on VXD Blog`, + keywords: post.tags?.join(', '), openGraph: { title: post.title, description: post.description || `Read ${post.title} on VXD Blog`, diff --git a/src/components/notion/BlockRenderer.tsx b/src/components/notion/BlockRenderer.tsx index 7f80820..2b89019 100644 --- a/src/components/notion/BlockRenderer.tsx +++ b/src/components/notion/BlockRenderer.tsx @@ -1,4 +1,5 @@ import { TextRenderer } from "./TextRenderer"; +import { CodeBlock } from "./CodeBlock"; import { cn } from "@/lib/utils"; import Image from "next/image"; import type { BlockObjectResponse, RichTextItemResponse, TableRowBlockObjectResponse, PartialBlockObjectResponse } from "@notionhq/client/build/src/api-endpoints"; @@ -92,12 +93,16 @@ export function BlockRenderer({ block }: BlockRendererProps) { ); case "code": + // Extract code text from rich text array + const codeText = value.rich_text + .map((rt: RichTextItemResponse) => rt.plain_text) + .join(''); + return ( -
-          
-            
-          
-        
+ ); case "image": const src = @@ -111,6 +116,7 @@ export function BlockRenderer({ block }: BlockRendererProps) { src={src} alt={caption || "Blog image"} className="object-cover w-full h-full" + unoptimized={true} /> {caption && ( diff --git a/src/components/notion/CodeBlock.tsx b/src/components/notion/CodeBlock.tsx new file mode 100644 index 0000000..aeb248f --- /dev/null +++ b/src/components/notion/CodeBlock.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useState } from 'react'; +import { Check, Copy } from 'lucide-react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +interface CodeBlockProps { + code: string; + language: string; +} + +export function CodeBlock({ code, language }: CodeBlockProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Language Badge */} + {language && ( +
+ {language} +
+ )} + + {/* Copy Button */} + + + {/* Code Block with Syntax Highlighting */} +
+ + {code} + +
+
+ ); +} diff --git a/src/lib/services/posts.service.ts b/src/lib/services/posts.service.ts index 573a49b..9adab50 100644 --- a/src/lib/services/posts.service.ts +++ b/src/lib/services/posts.service.ts @@ -5,6 +5,8 @@ import { notion } from "../notion"; import { BlogPost, SortOption, SortDirection } from "../types"; import { getNumberValue, extractBlogPostFromPage } from "./posts.helper"; +export const revalidate = 1 * 60 * 60; // Revalidate every 1 hour + export const getPostsDataSourceId = () => { const dataSourceId = process.env.NOTION_POSTS_DATA_SOURCE_ID; if (!dataSourceId) { @@ -43,7 +45,7 @@ const getCachedAllPosts = unstable_cache(async (): Promise => .map(extractBlogPostFromPage); return posts; -}, ['all-posts-v5'], { revalidate: 3600 }); +}, ['all-posts'], { revalidate }); export interface GetPublishedPostsOptions { tag?: string; @@ -235,7 +237,7 @@ export const getPageContent = unstable_cache(async (pageId: string) => { console.error("Failed to fetch page content:", error); return []; } -}, ['page-content'], { revalidate: 3600 }); +}, ['page-content'], { revalidate }); export const getPostById = unstable_cache(async (pageId: string): Promise => { try { @@ -255,7 +257,7 @@ export const getPostById = unstable_cache(async (pageId: string): Promise => { try { @@ -276,7 +278,7 @@ export const getPostBySlug = unstable_cache(async (slug: string): Promise {