A blog by Linghao Zhang, built with Next.js 15 and modern web technologies.
- Framework: Next.js 15 (App Directory, Static Export)
- Language: TypeScript
- Styling: Tailwind CSS 4
- Content: MDX for rich content
- Syntax Highlighting: Shiki
- Math: KaTeX
- Fonts: Inter Variable, Lora Italic Variable, Iosevka Fixed Curly
- Deployment: Cloudflare Pages
- โจ Modern, clean design with smooth view transitions
- ๐ฑ Fully responsive layout
- ๐จ Custom typography with variable fonts
- ๐ Flexoki-inspired color scheme (warm cream, near-black text, teal accents)
- ๐ MDX support for rich content
- ๐ Syntax highlighting for code blocks
- ๐งฎ Math rendering support
- ๐ท๏ธ Tag system with filtering
- ๐งฉ Projects page with SVG logos
- ๐ธ Photography gallery with lightbox
- ๐ก Auto-generated RSS feed
- ๐๏ธ Toggleable table of contents sidebar for long posts
- โ Anchor links on all headings
- ๐ Multilingual article support with language switcher
- โก Optimized static site with CDN delivery
- Node.js 18.18 or later
- npm
npm installnpm run devOpen http://localhost:3000 to view the blog.
npm run buildThe build outputs to the dist/ directory and includes:
- Static HTML/CSS/JS files
- Auto-generated RSS feed at
/feed.xml
# Generate RSS feed only
npm run rss
# Deploy to Cloudflare Pages (preview)
npm run deploy
# Deploy to Cloudflare Pages (production)
npm run deploy:prod
# Lint code
npm run lint.
โโโ app/ # Next.js app directory
โ โโโ _fonts/ # Custom font files
โ โโโ globals.css # Global styles
โ โโโ layout.tsx # Root layout
โ โโโ page.mdx # Home page
โ โโโ posts/ # Blog posts
โ โ โโโ page.tsx # Posts index
โ โ โโโ [slug]/ # Dynamic post pages
โ โ โโโ _articles/ # Post content (MDX)
โ โโโ notes/ # Legacy URL compatibility routes (redirect/alias to posts)
โ โ โโโ page.tsx # Legacy notes index
โ โ โโโ [slug]/ # Legacy note URLs
โ โโโ misc/ # Miscellaneous articles
โ โ โโโ page.tsx # Misc index
โ โ โโโ [slug]/ # Dynamic misc pages
โ โ โโโ _articles/ # Misc content (MDX)
โ โโโ gallery/ # Photography gallery
โ โ โโโ page.tsx # Gallery page
โ โ โโโ gallery-grid.tsx # Lightbox component
โ โ โโโ data.ts # Photo data
โ โโโ lists/ # Curation lists
โ โ โโโ page.tsx # Lists hub
โ โ โโโ [slug]/ # Individual list pages
โ โโโ projects/ # Projects page
โ โ โโโ page.tsx # Projects page
โ โ โโโ data.tsx # Project metadata
โ โ โโโ logos.tsx # Project SVG logos
โ โโโ resume/ # Public resume at /resume
โ โ โโโ source.ts # Resume content source of truth
โ โ โโโ page.tsx # Resume HTML renderer
โ โโโ tags/ # Tag system
โ โโโ all/ # Tag filtering page
โโโ components/ # React components
โ โโโ navbar.tsx # Navigation
โ โโโ tag.tsx # Tag component
โ โโโ toc-sidebar.tsx # Table of contents sidebar
โ โโโ ... # Other components
โโโ lib/ # Utilities
โ โโโ articles.ts # Content loading
โ โโโ seo.ts # OG/social image resolution
โ โโโ tags.ts # Tag management
โโโ scripts/ # Build scripts
โ โโโ generate-rss.mjs # RSS generation
โโโ public/ # Static assets
โโโ dist/ # Build output
โโโ mdx-components.tsx # MDX component config
โโโ next.config.ts # Next.js config
โโโ wrangler.toml # Cloudflare config
- Create a new
.mdxfile inapp/posts/_articles/ - Add metadata at the top:
export const metadata = {
title: 'Your Post Title',
description: 'A brief description',
date: '2025.01.01',
tags: ['Tag1', 'Tag2'],
// Optional: show a table of contents sidebar (default open)
toc: true,
// Optional: OG/social card image (hosted on Cloudflare R2 or any public URL)
image: 'https://r2.linghao.io/blog-assets/your-image.png',
imageWidth: 1200,
imageHeight: 630,
// Optional: multilingual linking (see Multilingual Support below)
language: 'en',
translationId: 'shared-slug',
canonical: true,
}
Your content here...- The post will automatically:
- Appear in the posts index
- Be included in RSS feed
- Be filterable by tags
- Get a URL like
/posts/your-post-title - Have anchor links on all headings (
#heading-slug)
Any post with two or more headings gets a toggleable TOC sidebar. A small icon button appears on the right side of the viewport; clicking it opens a panel listing all headings (h2โh4) with hierarchical numbering and active-heading highlighting as you scroll.
By default the sidebar is closed. Set toc: true in the article metadata to have it open automatically โ useful for long reference posts.
Articles sharing a translationId are grouped as translations of each other. The article page shows a language switcher between them. Only the canonical: true version appears in index pages and tag pages.
export const metadata = {
title: 'My Post',
language: 'en',
translationId: 'my-post',
canonical: true,
}- Posts (
app/posts/_articles/) - Blog posts and reading notes - Misc (
app/misc/_articles/) - Miscellaneous content
Legacy /notes/* URLs are still supported for backward compatibility, but new content should be added under posts.
The public resume is available at /resume. Its content source is app/resume/source.ts, and the rendered HTML page is app/resume/page.tsx. Update content in source.ts; only edit page.tsx for layout/styling changes. See docs/resume.md for the full update workflow.
The gallery feature displays a collection of photography. Photos are managed in app/gallery/data.ts.
To add a new photo:
- Upload the image to a hosting service (e.g., Cloudflare R2).
- Add a new object to the
photosarray inapp/gallery/data.ts:
{
id: 'unique-id',
src: 'https://your-image-url.jpg',
alt: 'Description for accessibility',
caption: 'Optional caption text',
metadata: {
'Location': 'Kyoto, Japan',
'Date': '2024-10-28',
'Camera': 'Sony ฮฑ7C II',
}
}MDX files can use React components:
<Card
image="https://example.com/image.jpg"
title="Card Title"
desc="Description"
link="https://example.com"
/>
<BlockSideTitle title="Side note text">
Main content here
</BlockSideTitle>The blog uses the "rurikon" color palette defined in app/globals.css, mapped to Flexoki values. Customize colors by modifying the @theme section. See docs/visual-style.md for the full palette reference.
Fonts are loaded locally via next/font/local in app/layout.tsx and exposed as CSS variables:
--sans: Inter Variable (body text, headings, UI elements โ used everywhere)--serif: Lora Italic Variable (navigation elements via globalnav {}rule)--mono: Iosevka Fixed Curly (code blocks and inline code)
Edit components/navbar.tsx to modify navigation links.
# Deploy to preview
npm run deploy
# Deploy to production
npm run deploy:prodSee DEPLOYMENT.md for detailed setup instructions.
Add tags to any article's metadata:
export const metadata = {
title: 'My Post',
tags: ['JavaScript', 'React', 'Web Development'],
}- Browse all tags:
/tags/all - Filter by tags:
/tags/all?tag=JavaScript - Click tags on articles to filter
The RSS feed is automatically generated on every build:
- Feed URL:
https://linghao.io/feed.xml - Includes: All posts and misc articles
- Updates: Automatic on deployment
- Manual generation:
npm run rss
MIT
This blog is inspired by Shu Ding's blog.