Skip to content

Commit 4cc2fe8

Browse files
feat(pages): add blog section with i18n support (#355)
Add a blog feature to the pages site including: - BlogPage component with list/detail views, tag filtering, search, and TOC - Blog content system with markdown posts and i18n (en/ja/zh) - Navbar integration with blog tab and improved active state detection - MarkdownRenderer image path handling for relative/absolute paths - Webpack CopyPlugin for serving static blog assets
1 parent 802af6b commit 4cc2fe8

15 files changed

Lines changed: 1248 additions & 2 deletions

File tree

pages/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@types/three": "^0.185.0",
3232
"autoprefixer": "^10.4.16",
3333
"babel-loader": "^9.1.3",
34+
"copy-webpack-plugin": "^14.0.0",
3435
"css-loader": "^6.8.1",
3536
"html-webpack-plugin": "^5.5.3",
3637
"postcss": "^8.5.15",
769 KB
Loading

pages/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import FeaturesPage from './pages/FeaturesPage';
55
import BenchmarkPage from './pages/BenchmarkPage';
66
import QuickStartPage from './pages/QuickStartPage';
77
import DocsPage from './pages/DocsPage';
8+
import BlogPage from './pages/BlogPage';
89

910
const ScrollToTop: React.FC = () => {
1011
const { pathname } = useLocation();
@@ -24,6 +25,8 @@ const App: React.FC = () => {
2425
<Route path="/quickstart" element={<LandingPage><QuickStartPage /></LandingPage>} />
2526
<Route path="/docs" element={<DocsPage />} />
2627
<Route path="/docs/:slug" element={<DocsPage />} />
28+
<Route path="/blog" element={<BlogPage />} />
29+
<Route path="/blog/:slug" element={<BlogPage />} />
2730
</Routes>
2831
</>
2932
);

pages/src/components/MarkdownRenderer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,15 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => {
7878
}, [toastVisible]);
7979

8080
const html = useMemo(() => {
81+
const publicPath = (window.location.pathname.replace(/\/[^/]*$/, '') || '').replace(/\/$/, '');
8182
// Custom renderer to generate heading IDs matching the TOC extraction logic
8283
const renderer = new Renderer();
84+
renderer.image = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
85+
const escapeAttr = (s: string) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
86+
const src = href.startsWith('/') ? `${publicPath}${href}` : href;
87+
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
88+
return `<img src="${escapeAttr(src)}" alt="${escapeAttr(text)}"${titleAttr} />`;
89+
};
8390
renderer.heading = function ({ text, depth }: { text: string; depth: number }) {
8491
const id = generateHeadingId(text);
8592
// Escape id attribute value to prevent XSS

pages/src/components/Navbar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const navTabs = [
1818
{ path: '/benchmark', labelKey: 'navbar.benchmark' },
1919
{ path: '/quickstart', labelKey: 'navbar.quickstart' },
2020
{ path: '/docs', labelKey: 'navbar.docs' },
21+
{ path: '/blog', labelKey: 'navbar.blog' },
2122
];
2223

2324
const Navbar: React.FC = () => {
@@ -79,7 +80,7 @@ const Navbar: React.FC = () => {
7980
{!isMobile && (
8081
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
8182
{navTabs.map((tab) => {
82-
const isActive = currentPath === tab.path;
83+
const isActive = tab.path === '/' ? currentPath === '/' : currentPath.startsWith(tab.path);
8384
return (
8485
<button
8586
key={tab.path}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
---
2+
title: Introducing the Open Code Review Blog System
3+
date: 2026-07-01
4+
tags: [announcement, guide]
5+
summary: Learn how the Open Code Review blog system works and how you can contribute articles using simple Markdown files managed in GitHub.
6+
author: lizhengfeng101
7+
---
8+
9+
## Why We Built This Blog
10+
11+
Open Code Review has many complex design decisions behind it — why certain features exist, why others were deliberately left out, and why we chose one approach over another.
12+
When we choose one architecture over another, when we decide NOT to implement something, when we find a better approach after hitting a wall — we want these stories to be recorded.
13+
That's why we created this blog — to share the design thinking and philosophy behind OCR's features.
14+
15+
Beyond OCR internals, we'll also share insights on cutting-edge AI techniques we explore and apply — prompt engineering strategies, model evaluation methods, and lessons learned from building AI-powered developer tools at scale.
16+
17+
## What Is This Blog
18+
19+
This blog is built into the Open Code Review website. All articles are managed as Markdown files in the GitHub repository — no CMS, no database, merge a PR and it's live.
20+
21+
If you can write Markdown and have deep insights on technology, you can publish here.
22+
23+
## How It Works
24+
25+
### File-Based Content
26+
27+
Each blog post is a `.md` file stored under `pages/src/content/blog/`. The directory structure looks like:
28+
29+
```
30+
pages/src/content/blog/
31+
├── index.ts # Content registry
32+
├── en/
33+
│ └── my-post.md # English version
34+
├── zh/
35+
│ └── my-post.md # Chinese version
36+
└── ja/
37+
└── my-post.md # Japanese version
38+
```
39+
40+
### Frontmatter Metadata
41+
42+
Every post starts with a YAML frontmatter block that defines its metadata:
43+
44+
```yaml
45+
---
46+
title: Your Article Title
47+
date: 2026-07-10
48+
tags: [tag1, tag2]
49+
summary: A one-line description shown on the list page.
50+
author: Your Name
51+
---
52+
```
53+
54+
| Field | Required | Description |
55+
|-------|----------|-------------|
56+
| title | Yes | Article title displayed on list and detail pages |
57+
| date | Yes | Publication date in YYYY-MM-DD format, used for sorting |
58+
| tags | No | Array of tags for filtering |
59+
| summary | No | Brief description shown in the article card |
60+
| author | No | Author name |
61+
62+
### Multi-Language Support
63+
64+
The blog supports English, Chinese, and Japanese. Place translated versions of the same article in the corresponding language directory with the same filename.
65+
66+
## How to Contribute a Post
67+
68+
### Step 1: Create Your Markdown File
69+
70+
Create a new `.md` file under `pages/src/content/blog/en/` (and optionally `zh/` and `ja/` for translations). Choose a slug-friendly filename like `my-article-topic.md`.
71+
72+
### Step 2: Register the Post
73+
74+
Open `pages/src/content/blog/index.ts` and:
75+
76+
1. Add your slug to the `BlogSlug` union type
77+
2. Import your markdown file(s)
78+
3. Add the entry to each language's post record
79+
80+
```typescript
81+
// Add to BlogSlug type
82+
export type BlogSlug =
83+
| 'introducing-ocr-blog'
84+
| 'my-article-topic'; // your new slug
85+
86+
// Add imports
87+
import enMyArticle from './en/my-article-topic.md';
88+
import zhMyArticle from './zh/my-article-topic.md';
89+
90+
// Add to post records
91+
const enPosts: Record<BlogSlug, string> = {
92+
'introducing-ocr-blog': enIntroducingOcr,
93+
'my-article-topic': enMyArticle,
94+
};
95+
```
96+
97+
If your article needs images, place them under `pages/public/images/blog/<your-slug>/`:
98+
99+
```
100+
pages/public/images/blog/
101+
└── my-article-topic/
102+
├── screenshot.png
103+
└── diagram.svg
104+
```
105+
106+
Then reference them in markdown with an absolute path:
107+
108+
```markdown
109+
![OCR Homepage](/images/blog/my-article-topic/screenshot.png)
110+
```
111+
112+
Here's an example of how an image renders:
113+
114+
![Open Code Review Homepage](/images/blog/introducing-ocr-blog/demo.png)
115+
116+
### Step 3: Submit a PR
117+
118+
Submit a Pull Request on [GitHub](https://github.com/alibaba/open-code-review). Once the PR is merged, your article will go live within minutes.
119+
120+
## Features
121+
122+
The blog system provides:
123+
124+
- **Tag filtering** — readers can filter posts by tag on the list page
125+
- **Full-text search** — Cmd+K opens a search modal
126+
- **Table of contents** — detail pages show a right-side TOC with scroll tracking
127+
- **Responsive design** — works on desktop and mobile
128+
- **Dark theme** — consistent with the rest of the OCR website
129+
130+
## Writing Tips
131+
132+
To make your article easier to publish, please follow these tips:
133+
134+
- Use `##` and `###` headings — they appear in the right-side table of contents
135+
- Keep summaries under 100 characters for clean card display
136+
- Tags should be lowercase and concise
137+
- Code blocks with language hints get syntax highlighting
138+
- Internal links use relative paths like `(/docs/quickstart)`
139+
140+
We look forward to your insights!

pages/src/content/blog/index.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* Blog content index — imports all markdown files and provides lookup by slug + language */
2+
3+
// English posts
4+
import enIntroducingOcr from './en/introducing-ocr-blog.md';
5+
6+
// Chinese posts
7+
import zhIntroducingOcr from './zh/introducing-ocr-blog.md';
8+
9+
// Japanese posts
10+
import jaIntroducingOcr from './ja/introducing-ocr-blog.md';
11+
12+
export type BlogSlug =
13+
| 'introducing-ocr-blog';
14+
15+
export interface BlogMeta {
16+
slug: BlogSlug;
17+
title: string;
18+
date: string;
19+
tags: string[];
20+
summary: string;
21+
author: string;
22+
}
23+
24+
const enPosts: Record<BlogSlug, string> = {
25+
'introducing-ocr-blog': enIntroducingOcr,
26+
};
27+
28+
const zhPosts: Record<BlogSlug, string> = {
29+
'introducing-ocr-blog': zhIntroducingOcr,
30+
};
31+
32+
const jaPosts: Record<BlogSlug, string> = {
33+
'introducing-ocr-blog': jaIntroducingOcr,
34+
};
35+
36+
const blogMap: Record<string, Record<BlogSlug, string>> = {
37+
en: enPosts,
38+
zh: zhPosts,
39+
ja: jaPosts,
40+
};
41+
42+
function stripFrontmatter(md: string): string {
43+
if (md.startsWith('---')) {
44+
const end = md.indexOf('---', 3);
45+
if (end !== -1) {
46+
return md.slice(end + 3).trim();
47+
}
48+
}
49+
return md;
50+
}
51+
52+
function parseFrontmatter(slug: BlogSlug, raw: string): BlogMeta {
53+
const defaults: BlogMeta = { slug, title: slug, date: '', tags: [], summary: '', author: '' };
54+
if (!raw.startsWith('---')) return defaults;
55+
const end = raw.indexOf('---', 3);
56+
if (end === -1) return defaults;
57+
const fm = raw.slice(3, end);
58+
59+
const titleMatch = fm.match(/title:\s*(.+)/);
60+
const dateMatch = fm.match(/date:\s*(.+)/);
61+
const summaryMatch = fm.match(/summary:\s*(.+)/);
62+
const authorMatch = fm.match(/author:\s*(.+)/);
63+
const tagsMatch = fm.match(/tags:\s*\[([^\]]*)\]/);
64+
65+
return {
66+
slug,
67+
title: titleMatch ? titleMatch[1].trim() : slug,
68+
date: dateMatch ? dateMatch[1].trim() : '',
69+
tags: tagsMatch ? tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean) : [],
70+
summary: summaryMatch ? summaryMatch[1].trim() : '',
71+
author: authorMatch ? authorMatch[1].trim() : '',
72+
};
73+
}
74+
75+
function getRawContent(slug: BlogSlug, language: string): string {
76+
const langPosts = blogMap[language] || blogMap.en;
77+
return langPosts[slug] || enPosts[slug] || '';
78+
}
79+
80+
export function getBlogContent(slug: BlogSlug, language: string): string {
81+
return stripFrontmatter(getRawContent(slug, language));
82+
}
83+
84+
export function getBlogMeta(slug: BlogSlug, language: string): BlogMeta {
85+
return parseFrontmatter(slug, getRawContent(slug, language));
86+
}
87+
88+
export function getAllBlogMetas(language: string): BlogMeta[] {
89+
const slugs = Object.keys(blogMap.en) as BlogSlug[];
90+
return slugs
91+
.map(slug => getBlogMeta(slug, language))
92+
.sort((a, b) => b.date.localeCompare(a.date));
93+
}
94+
95+
export function getAllTags(language: string): string[] {
96+
const metas = getAllBlogMetas(language);
97+
const tagSet = new Set<string>();
98+
metas.forEach(m => m.tags.forEach(t => tagSet.add(t)));
99+
return Array.from(tagSet).sort();
100+
}
101+
102+
export function searchBlog(query: string, language: string): { slug: BlogSlug; title: string; snippet: string }[] {
103+
if (!query.trim()) return [];
104+
const langPosts = blogMap[language] || blogMap.en;
105+
const results: { slug: BlogSlug; title: string; snippet: string }[] = [];
106+
const lowerQuery = query.toLowerCase();
107+
const slugs = Object.keys(langPosts) as BlogSlug[];
108+
for (const slug of slugs) {
109+
const raw = langPosts[slug] || enPosts[slug] || '';
110+
const meta = getBlogMeta(slug, language);
111+
const content = stripFrontmatter(raw);
112+
const lowerContent = content.toLowerCase();
113+
const lowerTitle = meta.title.toLowerCase();
114+
const lowerSummary = (meta.summary || '').toLowerCase();
115+
116+
let snippet = '';
117+
const contentIdx = lowerContent.indexOf(lowerQuery);
118+
if (lowerTitle.includes(lowerQuery)) {
119+
snippet = meta.title;
120+
} else if (lowerSummary.includes(lowerQuery)) {
121+
snippet = meta.summary || '';
122+
} else if (contentIdx !== -1) {
123+
const start = Math.max(0, contentIdx - 30);
124+
const end = Math.min(content.length, contentIdx + query.length + 60);
125+
snippet = content.slice(start, end).replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ').trim();
126+
if (start > 0) snippet = '...' + snippet;
127+
if (end < content.length) snippet = snippet + '...';
128+
} else {
129+
continue;
130+
}
131+
results.push({ slug, title: meta.title, snippet });
132+
}
133+
return results;
134+
}

0 commit comments

Comments
 (0)