Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion content/blog/2025/17_dockerfirst.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ author: mCell

![085.webp](https://stack-mcell.tos-cn-shanghai.volces.com/085.webp)


## **为什么我们需要 Docker?**

在开发中,你是否遇到过这些问题?
Expand Down
3 changes: 2 additions & 1 deletion src/app/topics/[topic]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const topicMeta: Record<
bun: {
title: 'Bun 指南',
meta: 'Runtime',
description: '这是一组面向 Node/JavaScript 开发者的 Bun 实战笔记:目标不是“百科全书”,而是让你能更快把 Bun 用在 CLI、脚本和小型服务里。',
description:
'这是一组面向 Node/JavaScript 开发者的 Bun 实战笔记:目标不是“百科全书”,而是让你能更快把 Bun 用在 CLI、脚本和小型服务里。',
},
}

Expand Down
50 changes: 35 additions & 15 deletions src/components/MermaidBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use client'

import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState, useRef } from 'react'
import { Copy, Maximize2, X } from 'lucide-react'

type MermaidBlockProps = {
code: string
}

// Cache mermaid instance to avoid re-importing
let mermaidInstance: typeof import('mermaid').default | null = null
let mermaidInitialized = false

export function MermaidBlock({ code }: MermaidBlockProps) {
const [svg, setSvg] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
Expand All @@ -16,33 +20,49 @@ export function MermaidBlock({ code }: MermaidBlockProps) {
)
const [copied, setCopied] = useState(false)
const [zoomed, setZoomed] = useState(false)
const isRenderingRef = useRef(false)

useEffect(() => {
let mounted = true

;(async () => {
// Prevent duplicate renders
if (isRenderingRef.current) return
isRenderingRef.current = true

try {
const mermaid = (await import('mermaid')).default
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: 'dark',
fontFamily: 'Source Sans 3, Inter, -apple-system, sans-serif',
themeVariables: {
background: '#0b0d11',
primaryColor: '#1a73e8',
primaryTextColor: '#e5e7eb',
lineColor: '#94a3b8',
},
})
const { svg } = await mermaid.render(renderId, code)
// Load and initialize mermaid only once
if (!mermaidInstance) {
const mermaid = (await import('mermaid')).default
mermaidInstance = mermaid
}

if (!mermaidInitialized) {
mermaidInstance.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: 'dark',
fontFamily: 'Source Sans 3, Inter, -apple-system, sans-serif',
themeVariables: {
background: '#0b0d11',
primaryColor: '#1a73e8',
primaryTextColor: '#e5e7eb',
lineColor: '#94a3b8',
},
})
mermaidInitialized = true
}

const { svg } = await mermaidInstance.render(renderId, code)
if (mounted) {
setSvg(svg)
setError(null)
}
} catch (err) {
console.error('Mermaid render failed', err)
if (mounted) setError('Mermaid 图渲染失败')
} finally {
isRenderingRef.current = false
}
})()

Expand Down
16 changes: 6 additions & 10 deletions src/components/PagefindSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,19 +157,15 @@ export function PagefindSearch({

try {
const search = await pagefind.search(query)
// Limit to 20 results and process them efficiently
const topResults = search.results.slice(0, 20)
const detailed = await Promise.all(
search.results.slice(0, 20).map(async (result: PagefindHit, idx) => {
topResults.map(async (result: PagefindHit, idx) => {
const data = await result.data()
return {
url: data.url,
title:
(data.meta && typeof data.meta.title === 'string'
? data.meta.title
: data.url) ?? `结果 ${idx + 1}`,
excerpt:
typeof data.excerpt === 'string'
? data.excerpt
: data.content?.slice(0, 200),
title: data.meta?.title || data.url || `结果 ${idx + 1}`,
excerpt: data.excerpt || data.content?.slice(0, 200),
}
}),
)
Expand All @@ -180,7 +176,7 @@ export function PagefindSearch({
} finally {
setIsSearching(false)
}
}, 180)
}, 200) // Slightly longer debounce for better performance

return () => clearTimeout(handle)
}, [query, isActive, ensurePagefind])
Expand Down
34 changes: 19 additions & 15 deletions src/components/ThreeBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ function ParticleNet() {
const time = clock.getElapsedTime()

if (pointsRef.current) {
// 1. 移除整体倾斜交互,保持网格平稳
// 平滑恢复到初始角度 (0,0,0)
// Smooth rotation recovery to initial angle (0,0,0)
pointsRef.current.rotation.x = THREE.MathUtils.lerp(
pointsRef.current.rotation.x,
0,
Expand All @@ -98,35 +97,40 @@ function ParticleNet() {
const positions = pointsRef.current.geometry.attributes.position
.array as Float32Array

// Use global mouse ref
// 将归一化的鼠标坐标 (-1 到 1) 转换为世界坐标
// Use global mouse ref and convert to world coordinates
const mouseX = (mouseRef.current.x * viewport.width) / 2
const mouseY = (mouseRef.current.y * viewport.height) / 2

const base = baseCoordsRef.current

// Pre-calculate values used in loop
const timeFactor1 = time * 0.8
const timeFactor2 = time * 0.6
const interactionRadius = 4.0
const interactionRadiusSq = interactionRadius * interactionRadius

for (let idx = 0; idx < totalPoints; idx++) {
const positionIndex = idx * 3
const baseIndex = idx * 2

const x = base[baseIndex]
const y = base[baseIndex + 1]

// 基础波浪运动
// Base wave motion
let z =
Math.sin(x * 0.6 + time * 0.8) * Math.cos(y * 0.6 + time * 0.6) * 0.8
Math.sin(x * 0.6 + timeFactor1) *
Math.cos(y * 0.6 + timeFactor2) *
0.8

// 鼠标交互:计算粒子到鼠标的距离
// Mouse interaction: calculate distance to mouse
const dx = x - mouseX
const dy = y - mouseY
const dist = Math.sqrt(dx * dx + dy * dy)

// 交互范围和强度
// 范围半径约 3 单位,中心强度 2.5
const interactionRadius = 4.0
if (dist < interactionRadius) {
// 使用高斯衰减函数制造平滑隆起
const strength = 2.5 * Math.exp((-dist * dist) / (2 * 1.5)) // sigma^2 = 1.5
const distSq = dx * dx + dy * dy

// Only apply interaction if within radius (using squared distance to avoid sqrt)
if (distSq < interactionRadiusSq) {
// Gaussian decay function for smooth bump
const strength = 2.5 * Math.exp(-distSq / 3.0) // sigma^2 = 1.5
z += strength
}

Expand Down
61 changes: 39 additions & 22 deletions src/lib/mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,22 @@ export interface Post {
slug: string
}

// Cache for post slugs to avoid repeated file system traversal
const slugCache = new Map<PostType, string[]>()
// Cache for parsed posts to avoid re-reading and re-parsing files
const postCache = new Map<string, Post>()

export function getPostSlugs(type: PostType) {
// Return cached result if available
if (slugCache.has(type)) {
return slugCache.get(type)!
}

const dir = path.join(contentDir, type)
if (!fs.existsSync(dir)) return []
if (!fs.existsSync(dir)) {
slugCache.set(type, [])
return []
}

const files: string[] = []

Expand All @@ -45,24 +58,16 @@ export function getPostSlugs(type: PostType) {
}

traverse(dir)
slugCache.set(type, files)
return files
}

export function getPostBySlug(type: PostType, slug: string): Post {
// Slug might contain slashes if it was nested, but here we usually flatten or handle it.
// However, the current implementation of [slug] page assumes a single segment slug.
// If we want to support nested routes like /blog/2025/foo, we need [...slug].
// For now, let's assume we want to flatten them or just find the file by name?
// Or better, let's update [slug] to [...slug] if we want to keep the structure.
// BUT, the user's existing structure is `blog/2025/foo.md`.
// If I return `2025/foo.md` as slug, the URL will be `/blog/2025%2Ffoo`.
// That's ugly.
// If I want `/blog/foo`, I need to handle collisions.
// Let's assume we want to support the path as is.
// So I should change `[slug]` to `[...slug]`.

// For now, I will implement a simple recursive search that returns the RELATIVE path as the slug.
// And I will update the page to use `[...slug]`.
// Check cache first
const cacheKey = `${type}:${slug}`
if (postCache.has(cacheKey)) {
return postCache.get(cacheKey)!
}

const dir = path.join(contentDir, type)
const realSlug = slug.replace(/\.mdx?$/, '')
Expand Down Expand Up @@ -100,7 +105,7 @@ export function getPostBySlug(type: PostType, slug: string): Post {
}
}

return {
const post: Post = {
slug: realSlug,
metadata: {
...data,
Expand All @@ -113,6 +118,10 @@ export function getPostBySlug(type: PostType, slug: string): Post {
},
content,
}

// Cache the post
postCache.set(cacheKey, post)
return post
}

export function getAllPosts(type: PostType): Post[] {
Expand All @@ -126,22 +135,30 @@ export function getAllPosts(type: PostType): Post[] {
export function getTopicSlugs(): string[] {
const dir = path.join(contentDir, 'topics')
if (!fs.existsSync(dir)) return []
return fs
.readdirSync(dir)
.filter((item) => fs.statSync(path.join(dir, item)).isDirectory())

// Cache directory check with readdirSync for better performance
const items = fs.readdirSync(dir, { withFileTypes: true })
return items.filter((item) => item.isDirectory()).map((item) => item.name)
}

export function getTopicPosts(topicSlug: string): Post[] {
const all = getAllPosts('topics')
const filtered = all.filter((post) => post.slug.startsWith(`${topicSlug}/`))
// Get only slugs for this topic, avoiding loading all posts
const slugs = getPostSlugs('topics')
const topicPrefix = `${topicSlug}/`

// Filter slugs first before loading posts
const filteredSlugs = slugs.filter((slug) => slug.startsWith(topicPrefix))

// Load only the relevant posts
const posts = filteredSlugs.map((slug) => getPostBySlug('topics', slug))

const getLeadingNumber = (slug: string) => {
const last = slug.split('/').pop() ?? slug
const match = last.match(/^(\d+)/)
return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER
}

return filtered.sort((a, b) => {
return posts.sort((a, b) => {
const numA = getLeadingNumber(a.slug)
const numB = getLeadingNumber(b.slug)
if (numA !== numB) return numA - numB
Expand Down
5 changes: 4 additions & 1 deletion src/types/pagefind.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ declare module '/pagefind/pagefind.js' {
}

export function init(): Promise<void>
export function options(opts?: { basePath?: string; baseUrl?: string }): Promise<void>
export function options(opts?: {
basePath?: string
baseUrl?: string
}): Promise<void>
export function search(query: string): Promise<{ results: PagefindHit[] }>
}