This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an Astro v5 blog (sh-blog-next) with content collections, MDX support, React integration, and shadcn/ui components. The site is deployed to Cloudflare Pages using Wrangler.
Package Manager: Always use pnpm (not npm/yarn) to maintain lockfile consistency.
pnpm install— Install dependenciespnpm dev— Start dev server with hot reloadpnpm build— Production build (includes Pagefind search index generation viapostbuild)pnpm preview— Preview production build locallypnpm astro check— Check content/schema issues and TypeScript errorspnpm deploy— Deploy to Cloudflare Pages (wrangler deploy --assets=./dist)pnpm cn— Run shadcn CLI to add UI components (pnpm dlx shadcn@latest)
Content Location: src/content/blog/ contains .md and .mdx files.
Schema Definition: src/content.config.ts defines the blog collection with:
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' })- Zod schema enforcing frontmatter fields:
title,description,pubDate,updatedDate(optional),heroImage(optional),tags(optional array),category(optional string or array),draft(optional boolean) - Uses
z.coerce.date()for date parsing andimage()for responsive images
Content Access Pattern:
import { getCollection, render } from 'astro:content';
// Get all posts
const posts = await getCollection('blog');
// Render a single post
const { Content, headings } = await render(post);Important: Post IDs are derived from file names (e.g., first-post.md → id: first-post). URLs follow pattern /blog/${post.id}/.
Site Config: shblog.config.ts centralizes:
- Site metadata (title, description, lang, favicon)
- Author info (name, bio, avatar, social links)
- Navigation links with Lucide icons
- Style defaults (postsPerPage, titleSeparator, defaultPostImage)
Constants: src/consts.ts re-exports config values for easy importing (e.g., SITE_TITLE, DEFAULT_POST_IMAGE, SEPARATOR).
Astro Config: astro.config.mjs defines:
- Integrations:
AutoImport()withastroAsides(),expressiveCode(),mdx(),sitemap(),react(),pagefind(),partytown(),metaTags(),devtoolBreakpoints() - Custom remark/rehype plugins (see below)
- Site URL: Defaults to
https://sh-blog-next.vercel.app(can be overridden viaSITE_URLenvironment variable)
Remark Plugins (in order):
remarkMath— Math notation supportremarkReadingTime— InjectsminutesReadinto frontmatterremarkGithubAdmonitionsToDirectives→remarkDirective— GitHub-style admonitionsremarkSectionize— Wrap sections for stylingremarkSpoiler(custom) — Spoiler syntax
Rehype Plugins (in order):
rehypeKatex— Render math with KaTeX (requireskatex/dist/katex.min.css)rehypeCodeTitles— Code block titlesrehypeCodeBlock(custom) — Custom code block processingrehypeComponentswithAdmonitionComponent— Custom admonition rendering (note, tip, important, caution, warning)rehypeSlug→rehypeAutolinkHeadings— Heading anchors with#prepended
Plugin Order is Critical: Do not reorder plugins without understanding dependencies.
Located in src/plugins/:
remark-reading-time.mjs— Calculates reading time usingreading-timepackageremark-spoiler.js— Custom spoiler directive handlingshiki-code-metadata.mjs— Shiki transformer for code metadatarehype-code-block.mjs— Custom code block enhancementsrehype-component-admonition.mjs— Admonition component generator
BaseLayout (src/components/layouts/BaseLayout.astro):
- Root layout with
<BaseHead>,<Header>, and<Footer> - Handles meta tags, fonts, and global styles
BlogPost Layout (src/layouts/BlogPost.astro):
- Wraps BaseLayout
- Displays Hero section with hero image or default
- Shows post metadata (author, dates)
- Renders
<slot />for MDX content - Includes Sidebar and TOC (table of contents)
- Inline script for code copy functionality
Props Structure:
interface Props {
title: string;
description: string;
pubDate: Date;
updatedDate?: Date;
heroImage?: any;
headings: Array<{ depth: number; slug: string; text: string }>;
}src/components/core/— BaseHead, Header, Footer, HeaderLinksrc/components/layouts/— Layout wrapperssrc/components/content/— FormattedDate, Greeting, BlogList, Home/Hero componentssrc/components/markdown/— CodeBlock and markdown-specific componentssrc/components/ui/— shadcn/ui components (button, card, input, separator, etc.)src/components/widgets/— SiteRuntime widgetsrc/components/global/— ToTop buttonsrc/components/search/— Search components (in development)
shadcn/ui Integration:
- Components in
src/components/ui/follow shadcn conventions - Use
cn()utility fromsrc/lib/utils.tsfor class merging (clsx+tailwind-merge) - Add new components via
pnpm cncommand
Styling:
- Tailwind CSS v4 with
@tailwindcss/viteplugin - Global styles in
src/styles/global.css - Markdown-specific styles in
src/styles/markdown.css - Uses variable fonts:
@fontsource-variable/inter,@fontsource-variable/noto-sans-tc,@fontsource-variable/noto-serif-tc
TypeScript configured with @/* alias mapping to ./src/*:
import config from "shblog.config"; // root import
import { cn } from "@/lib/utils"; // alias importsrc/pages/index.astro— Homepagesrc/pages/about.astro— About page (uses BlogPost layout)src/pages/404.astro— Custom 404 with Easter eggssrc/pages/blog/index.astro— Blog listing pagesrc/pages/blog/[...slug].astro— Dynamic blog post pages (usesgetStaticPaths())src/pages/rss.xml.js— RSS feed generation
Responsive Images:
- Use
astro:assetswith<Image>component for optimization - Store assets in
src/assets/and reference with relative paths in frontmatter - Example:
heroImage: '../../assets/blog-placeholder-3.jpg'
Public Assets:
public/directory served as-is (fonts, favicon, default images)- BaseHead preloads fonts from
/fonts/
Create a Blog Post:
- Add file to
src/content/blog/(e.g.,my-new-post.md) - Include required frontmatter:
---
title: "Post Title"
description: "Brief description"
pubDate: "Dec 11 2025"
heroImage: "../../assets/my-image.jpg" # optional
---
Your content here...- Run
pnpm buildto validate schema - Post will be available at
/blog/my-new-post/
Adding Frontmatter Fields:
If you need new fields, update src/content.config.ts schema:
schema: ({ image }) =>
z.object({
// existing fields...
tags: z.array(z.string()).optional(), // new field
}),Target: Cloudflare Pages via Wrangler
Config: wrangler.jsonc specifies:
assets.directory: "./dist"compatibility_date: "2025-11-30"
Deploy Process:
pnpm build— Generate static site in./distpnpm deploy— Upload to Cloudflare Pages
Prefer Astro Over React: Use Astro components (.astro) for static content. Only use React (.tsx) when interactivity is required.
Security: When handling user input (e.g., search functionality), use dompurify to sanitize input. Package is already installed.
Code Comments: Codebase includes Chinese comments. Maintain this pattern when adding inline explanations.
Link Patterns: Always use trailing slashes for internal links (e.g., /blog/${post.id}/).
Image Defaults: If heroImage is missing, use DEFAULT_POST_IMAGE constant.
Title Formatting: Page titles follow pattern: ${title} ${SEPARATOR} ${config.title} (e.g., "Post Title - SamHacker Blog").
- Don't modify plugin order in
astro.config.mjswithout understanding dependencies - Don't hardcode site metadata — use
shblog.config.tsorsrc/consts.ts - Don't use npm/yarn — this breaks
pnpm-lock.yaml - Don't skip schema validation —
pnpm astro checkcatches content errors before build - Don't assume different frontmatter fields exist — always check
src/content.config.ts - Don't forget KaTeX CSS — Math rendering requires
katex/dist/katex.min.cssimport
- React 19 with
@astrojs/reactintegration - Use
client:*directives for hydration (e.g.,client:load,client:visible) - Radix UI primitives available: Avatar, Separator, Slot, Tooltip
- Lucide React for icons