The TOC toggle button was appearing near the end of posts instead of
floating at a fixed position. This happened because the button was
rendered inside the PostLayout component hierarchy.
Changes:
- Use React Portal to render TOC button at document.body level
- Add mounted state for proper SSR/client hydration
- Button now floats like back-to-top button, visible from start
This ensures the button is always visible and accessible, similar to
the back-to-top button behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Removed @next/bundle-analyzer package
- Removed build:analyze script
- Cleaned up next.config.mjs
The Next Bundle Analyzer is not compatible with Turbopack builds yet.
Since this project uses Turbopack, the analyzer cannot be used.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bundle analyzer requires webpack, add --webpack flag to build:analyze script.
Note: This makes analysis builds slower but enables bundle visualization.
Regular builds still use faster Turbopack.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Configure @next/bundle-analyzer for production bundle analysis:
**Changes:**
- Install @next/bundle-analyzer package
- Update next.config.mjs to wrap config with bundle analyzer
- Add npm script `build:analyze` to run build with ANALYZE=true
- Bundle analyzer only enabled when ANALYZE=true environment variable is set
**Usage:**
```bash
# Run build with bundle analysis
npm run build:analyze
# Opens interactive bundle visualization in browser
# Shows chunk sizes, module dependencies, and optimization opportunities
```
**Note:** Kept Mastodon feed as Client Component (not Server Component)
because formatRelativeTime() uses `new Date()` which requires dynamic
rendering. Converting to Server Component would prevent static generation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented comprehensive Schema.org structured data across the blog to improve SEO and enable rich snippets in search results.
Changes:
- Created JSON-LD helper component for safe schema rendering
- Added BlogPosting schema to blog posts with:
* Article metadata (headline, description, image, dates)
* Author and publisher information
* Keywords and article sections from tags
- Added BreadcrumbList schema to blog posts for navigation
- Added WebSite and Organization schemas to root layout
* Site-wide identity and branding
* Search action for site search functionality
- Added CollectionPage schema to homepage
* Blog collection metadata
- Added WebPage schema to static pages
* Page metadata with dates and images
Benefits:
- Rich snippets in Google/Bing search results
- Better content understanding by search engines
- Article cards with images, dates, authors in SERPs
- Breadcrumb navigation in search results
- Improved SEO ranking signals
All schemas validated against Schema.org specifications and include proper Chinese language support.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed all internal post links from /posts/ to /blog/ to match the actual URL structure. The contentlayer config generates URLs as /blog/{slug} for posts, not /posts/{slug}.
Fixed links in:
- content/pages/關於作者.md (7 links)
- content/posts/OPNsense 在 Proxmox VE 內安裝筆記.md (1 link)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The callout plugin wasn't working because:
1. Contentlayer cache was preventing the plugin from running
2. The plugin wasn't handling blockquotes with whitespace text nodes
3. The plugin needed to skip whitespace-only children to find actual content
Updated the rehype plugin to:
- Skip whitespace-only text nodes when looking for [!TYPE] markers
- Handle both direct text children and text within paragraphs
- Properly extract the callout type from regex match
- Clean up empty text nodes after removing markers
Now callouts render correctly with proper structure:
- Header with icon and title
- Content wrapper with styled box
- All 5 callout types supported (NOTE, TIP, IMPORTANT, WARNING, CAUTION)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The TOC component was only extracting headings once at mount, causing it to show stale headings when navigating between posts via client-side routing. Now it re-extracts headings whenever the pathname changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement proper GitHub-style callouts with beautiful styling:
Features:
- Custom rehype plugin to transform > [!NOTE] syntax
- Support for 5 callout types:
* NOTE (blue, 📝)
* TIP (green, 💡)
* IMPORTANT (purple, ❗)
* WARNING (orange, ⚠️)
* CAUTION (red, 🚨)
- Gradient backgrounds with accent colors
- Full dark mode support
- Converts existing emoji callouts to proper format
Files:
- lib/rehype-callouts.ts: Custom plugin for parsing
- contentlayer.config.ts: Add plugin to pipeline
- styles/globals.css: Beautiful styling for all types
- content/: Convert 2 emoji callouts to [!TIP] format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements essential blog features:
1. RSS Feed (/feed.xml)
- Latest 20 posts with full content
- Proper XML escaping and CDATA sections
- Includes tags, authors, and descriptions
- Auto-discovery link in HTML head
2. Sitemap (/sitemap.xml)
- All posts, pages, and tag pages
- Proper lastModified dates and priorities
- Automatic generation via Next.js built-in support
3. Robots.txt (/robots.txt)
- Allow all crawlers
- Disallow API and admin routes
- Links to sitemap for better SEO
4. Code Syntax Highlighting
- Using rehype-pretty-code + Shiki
- GitHub Dark/Light themes based on user preference
- Line numbers for all code blocks
- Support for highlighted lines
- Inline code styling
- Code title support
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous approach using an API route to serve Pagefind files
doesn't work on Vercel's serverless environment because fs.readFile
can't reliably access files in the deployed output.
Solution: Serve Pagefind files directly from public/_pagefind as
static assets, which is the standard Next.js approach and works
reliably on all deployment platforms.
Changes:
- Update search modal to load from /_pagefind/ instead of /pagefind/
- Remove app/pagefind/[...path]/route.ts API route (no longer needed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: Search modal hangs on loading in production deployment,
but works fine locally.
Root cause: The Pagefind route handler was reading files from
.next/pagefind, which is not reliably accessible in production
deployments. The build script copies Pagefind files to
public/_pagefind for deployment, but the route wasn't using them.
Solution: Changed the file path in app/pagefind/[...path]/route.ts
from .next/pagefind to public/_pagefind. Files in the public
directory are always accessible and properly deployed.
This ensures search works consistently across both development
and production environments.
Features:
- Display latest 5 Mastodon posts (toots) in sidebar
- Include original posts and boosts, exclude replies
- Show medium-length previews (150-200 chars)
- Styled to match existing blog design with purple Mastodon branding
- Section title: "微網誌 (Microblog)"
- Relative timestamps in Chinese ("2小時前")
- Links to original posts on Mastodon
- Loading skeletons for better UX
- Graceful error handling (fails silently if API unavailable)
- Respects dark mode
Implementation:
- Created lib/mastodon.ts with utility functions:
- Parse Mastodon URL format
- Strip HTML from content
- Smart text truncation
- Relative time formatting in Chinese
- API functions to fetch account and statuses
- Created components/mastodon-feed.tsx:
- Client component with useEffect for data fetching
- Fetches directly from Mastodon public API
- Handles boosts/reblogs with indicator
- Shows media attachment indicators
- Matches existing card styling patterns
- Updated components/right-sidebar.tsx:
- Added MastodonFeed between profile and hot tags
- Maintains consistent spacing and layout
Usage:
Set NEXT_PUBLIC_MASTODON_URL in .env.local to enable
Format: https://your.instance/@yourhandle
Problem: Table of Contents displayed headings from previously viewed
articles when navigating between posts via client-side routing.
Root cause: PostToc component's useEffect with empty dependency array
only ran once on mount, so it retained stale heading data when React
reused the component instance during navigation.
Solution: Add contentKey prop flow:
- Blog/page routes pass slug to PostLayout
- PostLayout passes contentKey as key prop to PostToc instances
- React remounts PostToc when key changes, rebuilding TOC correctly
Files changed:
- components/post-layout.tsx: Add contentKey prop and key forwarding
- app/blog/[slug]/page.tsx: Pass slug as contentKey
- app/pages/[slug]/page.tsx: Pass slug as contentKey
## Features
- Create /api/og route for dynamic Open Graph image generation
- Beautiful gradient design with site branding
- Display post title, description, and tags
- Support for both light and dark themes
- Proper sizing for social media (1200x630)
## Implementation
- Use @vercel/og package for image generation
- Add OpenGraph and Twitter Card metadata to blog posts
- Fallback to localhost for development
- Uses NEXT_PUBLIC_SITE_URL environment variable for production
## Social Media Support
- OpenGraph (Facebook, LinkedIn, etc.)
- Twitter Cards with large image preview
- Article metadata including publish time and tags
Example usage:
/api/og?title=Post+Title&description=Post+Desc&tags=tag1,tag2,tag3
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Use React Portal to render modal at document body level to avoid stacking context issues
- Increase z-index to z-[9999] to ensure modal appears on top of all content
- Add cleanup function to prevent duplicate Pagefind initializations
- Replace CSS class overrides with CSS variables for better maintainability
- Enhance search result text colors for improved readability in both light and dark modes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where Pagefind static files weren't accessible due to Next.js routing conflicts.
Solution:
- Created API route at app/pagefind/[...path]/route.ts to serve Pagefind files from .next/pagefind/
- Updated build script to copy pagefind index to public/_pagefind (backup)
- API route handles all /pagefind/* requests and serves files with proper content types
- Added caching headers for optimal performance
This resolves the "cannot type in search" issue - the search modal can now load Pagefind UI and index files correctly.
Technical Details:
- Next.js App Router was treating /pagefind/ as a route, returning 404
- Static files in public/ weren't accessible for subdirectories due to routing priority
- API route bypasses routing to serve .next/pagefind/* files directly
- Supports .js, .css, .json, .wasm, and Pagefind-specific file types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added autofocus configuration and manual focus call to ensure search input is immediately focusable when modal opens.
Changes:
- Added autofocus: true to PagefindUI config
- Added setTimeout to manually focus input after UI loads
- Ensures users can type immediately after opening search modal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Integrated Pagefind for static site search with built-in Chinese word segmentation support.
Changes:
1. **Installed Pagefind** (v1.4.0) as dev dependency
2. **Updated build script** to run Pagefind indexing after Next.js build
- Indexes all 69 pages with 5,711 words
- Automatic Chinese (zh-tw) language detection
3. **Created search modal component** (components/search-modal.tsx)
- Dynamic Pagefind UI loading (lazy-loaded on demand)
- Keyboard shortcuts (Cmd+K / Ctrl+K)
- Chinese translations for UI elements
- Dark mode compatible styling
4. **Added search button to header** (components/site-header.tsx)
- Integrated SearchButton with keyboard shortcut display
- Modal state management
5. **Custom Pagefind styles** (styles/globals.css)
- Tailwind-based styling to match site design
- Dark mode support
- Highlight styling for search results
Features:
- ✅ Full-text search across all blog posts and pages
- ✅ Built-in Chinese word segmentation (Unicode-based)
- ✅ Mixed Chinese/English query support
- ✅ Zero bundle impact (20KB lazy-loaded on search activation)
- ✅ Keyboard shortcuts (⌘K / Ctrl+K)
- ✅ Search result highlighting with excerpts
- ✅ Dark mode compatible
Technical Details:
- Pagefind runs post-build to index .next directory
- Search index stored in .next/pagefind/
- Chinese segmentation works automatically via Unicode boundaries
- No third-party services or API keys required
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed tag matching issue where tags with spaces and non-ASCII characters (like "Medicine - 醫學") were not working correctly on Vercel.
Changes:
1. Updated getTagSlug() to normalize tags without encoding - Next.js handles URL encoding automatically
2. Added decodeURIComponent() in tag page to decode incoming URL parameters
3. This ensures proper matching between generated slugs and URL parameters
The fix resolves:
- Tag archive pages showing wrong characters
- Articles not being collected under correct tags
- URL display issues with encoded characters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated getTagSlug() to properly encode tags with spaces and non-ASCII characters (like Chinese). The function now:
- Normalizes multiple spaces/dashes to single dashes
- Properly encodes non-ASCII characters using encodeURIComponent
- Prevents issues with URL encoding on Vercel deployment
This fixes tags like "Medicine - 醫學" being displayed as "medicine---%E9%86%AB%E5%AD%B8" by generating clean URLs like "medicine-%E9%86%AB%E5%AD%B8".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated ESLint from 8.57.1 to 9.39.1 to resolve peer dependency conflict with eslint-config-next@16.0.3 which requires ESLint >=9.0.0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Upgraded Next.js to v16, React to v19
- Migrated from contentlayer to contentlayer2
- Migrated to Turbopack by decoupling Contentlayer from webpack
- Updated all page components to handle async params (Next.js 15+ breaking change)
- Changed package.json to type: module and renamed config files to .cjs
- Updated README with current tech stack and article creation instructions
- Fixed tag encoding issue (removed double encoding)
- All security vulnerabilities resolved (npm audit: 0 vulnerabilities)