Skip to content

feat(pages): add blog section with i18n support#355

Merged
lizhengfeng101 merged 1 commit into
mainfrom
feat/pages-blog
Jul 10, 2026
Merged

feat(pages): add blog section with i18n support#355
lizhengfeng101 merged 1 commit into
mainfrom
feat/pages-blog

Conversation

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Summary

Add a blog feature to the pages site:

  • BlogPage component — list view with tag filtering and search modal (⌘K), detail view with TOC sidebar and scroll-spy heading highlight
  • Blog content system — markdown-based posts with frontmatter metadata, i18n support for en/ja/zh, centralized blog registry in pages/src/content/blog/index.ts
  • Navbar integration — added Blog tab, improved active state detection to use startsWith matching for nested routes
  • MarkdownRenderer enhancement — custom image renderer to handle absolute paths with correct public path resolution
  • Webpack config — added copy-webpack-plugin to serve static assets from pages/public/ directory

Includes an initial blog post "Introducing Open Code Review Blog" in all three languages.

Review notes

Code review flagged a few items worth addressing in follow-up:

  • publicPath calculation in MarkdownRenderer may produce incorrect paths under multi-level routes
  • searchBlog slug source inconsistency with getAllBlogMetas
  • Deprecated navigator.platform usage
  • Heavy inline styles that could be moved to CSS

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

@css521 css521 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@lizhengfeng101 lizhengfeng101 merged commit 4cc2fe8 into main Jul 10, 2026
7 checks passed
@lizhengfeng101 lizhengfeng101 deleted the feat/pages-blog branch July 10, 2026 14:55
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 10 issue(s) in this PR.

  • ✅ Successfully posted inline: 10 comment(s)

const renderer = new Renderer();
renderer.image = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
const escapeAttr = (s: string) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const src = href.startsWith('/') ? `${publicPath}${href}` : href;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null pointer exception: In the marked library, the href parameter passed to the image renderer can be null or undefined at runtime (e.g., for malformed markdown like ![alt]()). Although the TypeScript type annotation declares it as string, this does not enforce runtime safety. Calling href.startsWith('/') when href is null/undefined will throw a TypeError.

Consider adding a null guard before accessing href.

Suggestion:

Suggested change
const src = href.startsWith('/') ? `${publicPath}${href}` : href;
const safeHref = href ?? '';
const src = safeHref.startsWith('/') ? `${publicPath}${safeHref}` : safeHref;

<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
{navTabs.map((tab) => {
const isActive = currentPath === tab.path;
const isActive = tab.path === '/' ? currentPath === '/' : currentPath.startsWith(tab.path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startsWith check is fragile and could cause incorrect active state for tabs with overlapping prefixes. For example, if a future route like /docs-api or /benchmark-results is added, it would incorrectly highlight the /docs or /benchmark tab respectively.

A safer approach is to check that the current path either matches exactly or starts with the tab path followed by /:

Suggestion:

Suggested change
const isActive = tab.path === '/' ? currentPath === '/' : currentPath.startsWith(tab.path);
const isActive = currentPath === tab.path || currentPath.startsWith(tab.path + '/');

Comment thread pages/src/styles/blog.css
Comment on lines +15 to +18
.blog-card:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .blog-card has cursor: pointer indicating it's clickable, but no :focus or :focus-visible style is defined. In BlogPage.tsx, .blog-tag is rendered as a <button> element which is keyboard-focusable, but there's no visible focus indicator for it either. Consider adding focus styles for both interactive elements to support keyboard navigation.

For example:

.blog-tag:focus-visible {
  outline: 2px solid #2BDE5E;
  outline-offset: 2px;
}

Note: .blog-card in BlogPage.tsx is a plain <div> with onClick — it won't receive keyboard focus at all. That component should use a <button> or add role="button" + tabIndex={0} + keyboard handlers.

Comment on lines +88 to +93
export function getAllBlogMetas(language: string): BlogMeta[] {
const slugs = Object.keys(blogMap.en) as BlogSlug[];
return slugs
.map(slug => getBlogMeta(slug, language))
.sort((a, b) => b.date.localeCompare(a.date));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent slug sources across functions: getAllBlogMetas always derives slugs from blogMap.en, while searchBlog derives slugs from blogMap[language]. This means:

  1. If a blog post exists only in a non-English language (e.g., zh), it will never appear in list views (getAllBlogMetas) but could appear in search results (searchBlog).
  2. Conversely, if a post exists in English but not in another language, getAllBlogMetas will list it for all languages, silently falling back to English content via getRawContent.

Consider deriving slugs consistently — either always from the requested language's map, or by taking the union of all language maps — and documenting the fallback behavior.

Comment on lines +42 to +50
function stripFrontmatter(md: string): string {
if (md.startsWith('---')) {
const end = md.indexOf('---', 3);
if (end !== -1) {
return md.slice(end + 3).trim();
}
}
return md;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated frontmatter boundary detection: Both stripFrontmatter and parseFrontmatter independently locate the frontmatter delimiters using the same indexOf('---', 3) logic. Consider extracting a shared helper (e.g., extractFrontmatter(raw): { frontmatter: string; body: string } | null) to avoid duplication and ensure both functions stay consistent if the parsing logic changes.

Comment on lines +68 to +69
const isDetailView = !!(slugParam && validSlugs.has(slugParam as BlogSlug));
const activeSlug = isDetailView ? (slugParam as BlogSlug) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: No handling for invalid/missing blog slugs. When a user navigates to /blog/some-invalid-slug, isDetailView will be false (since the slug isn't in validSlugs), and the component silently falls through to the list view without any indication that the requested post was not found. This is confusing UX and masks routing issues.

Consider adding a 404/not-found state when slugParam is present but doesn't match a valid slug, or redirecting to /blog with an error message.

Comment on lines +42 to +43
const id = generateHeadingId(text);
headings.push({ id, text, level });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate heading IDs possible. The generateHeadingId function does not handle duplicate heading texts. If a blog post has two headings with the same text (e.g., two ## Overview sections), both will get the same ID. This causes:

  1. The IntersectionObserver to track the wrong element.
  2. TOC click-to-scroll to jump to the first matching element instead of the intended one.
  3. Invalid HTML (duplicate id attributes).

Consider appending a counter suffix for duplicates (e.g., overview-1, overview-2). Note that both extractHeadings here and the MarkdownRenderer heading renderer must use the same deduplication strategy to keep IDs consistent.

>
<img src={searchIcon} alt="" style={{ width: 16, height: 16, opacity: 0.6 }} />
<span>{t('blog.search.placeholder')}</span>
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.3)', fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif', lineHeight: 1, marginLeft: 8 }}>{navigator.platform?.includes('Mac') ? '⌘K' : 'Ctrl+K'}</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated API: navigator.platform is deprecated. This property is unreliable on newer browsers/platforms (e.g., iPadOS reports as MacIntel, ChromeOS may return unexpected values). Consider using navigator.userAgentData?.platform (with fallback) or a simple userAgent check for more reliable platform detection.

Comment on lines +193 to +205
<button
onClick={() => navigate('/blog')}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
marginBottom: 24,
fontSize: 14,
color: 'rgba(255,255,255,0.5)',
fontFamily,
transition: 'color 0.2s',
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline styles overuse: Many of these inline styles are static (e.g., the back button, title, metadata container, prev/next buttons, TOC container). They should be extracted to CSS classes in blog.css for better maintainability, reusability, and consistency with the existing pattern used for .blog-card, .blog-tag, etc. Only truly dynamic styles (like conditional padding based on isMobile or active/hover color states) should remain inline.

const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA') && !searchOpen) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: Cmd+K toggle inconsistency. The guard condition !searchOpen in the INPUT/TEXTAREA check means that when the search modal IS open and focus is on the search input, pressing Cmd+K won't toggle it closed (because the early return fires). Users who expect Cmd+K to act as a toggle will need to use Escape instead. Consider removing the !searchOpen condition so Cmd+K consistently toggles the modal regardless of focus state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants