Skip to content

feat(pages): add Docs page with search, markdown rendering, and i18n support#285

Closed
hezheng-max wants to merge 1 commit into
alibaba:mainfrom
hezheng-max:feat/docs-page-search
Closed

feat(pages): add Docs page with search, markdown rendering, and i18n support#285
hezheng-max wants to merge 1 commit into
alibaba:mainfrom
hezheng-max:feat/docs-page-search

Conversation

@hezheng-max

@hezheng-max hezheng-max commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
  • Add DocsPage with full-text search modal and keyboard shortcuts (⌘K)
  • Add MarkdownRenderer with DOMPurify sanitization and mermaid support
  • Add bilingual docs content (en/zh) for all sections
  • Add shared headingId utility with deduplication for consistent TOC anchors
  • Add search keyboard hints with i18n support
  • Update Navbar with Docs navigation link
  • Configure webpack for markdown imports
  • Extract parseFrontmatter helper to eliminate DRY violation
  • Move copy button and toast inline styles to CSS classes
  • Adjust HeroSection terminal section height and bottom padding

@github-actions github-actions Bot left a comment

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.

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

  • ✅ 9 posted as inline comment(s)
  • 📝 0 posted as summary

⚠️ 1 warning(s) occurred during review.

Comment thread pages/package.json
"@babel/preset-env": "^7.29.5",
"@babel/preset-react": "^7.23.5",
"@babel/preset-typescript": "^7.23.3",
"@types/dompurify": "^3.0.5",

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 type conflict: dompurify v3.x ships its own built-in TypeScript type definitions. Installing @types/dompurify alongside it may cause duplicate or conflicting type declarations. Consider removing @types/dompurify from devDependencies since the bundled types in dompurify@^3.4.11 should be sufficient.

Comment thread pages/src/i18n/en.ts Outdated
'docs.sidebar.gettingStarted': 'Getting Started',
'docs.sidebar.userGuide': 'User Guide',
'docs.sidebar.overview': 'Overview',
'docs.sidebar.quickstart': 'QuickStart',

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.

Minor inconsistency: The navbar uses 'Quick Start' (with a space) for navbar.quickstart, but here it's 'QuickStart' (no space). Consider using 'Quick Start' for consistency across the UI.

Suggestion:

Suggested change
'docs.sidebar.quickstart': 'QuickStart',
'docs.sidebar.quickstart': 'Quick Start',

Comment on lines +138 to +149
export function getDocTitle(slug: DocSlug, language: string): string {
const raw = getRawContent(slug, language);
if (raw.startsWith('---')) {
const end = raw.indexOf('---', 3);
if (end !== -1) {
const fm = raw.slice(3, end);
const match = fm.match(/title:\s*(.+)/);
if (match) return match[1].trim();
}
}
return slug;
}

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.

Duplicate Code / DRY Violation: The frontmatter detection logic here (startsWith('---') + indexOf('---', 3)) is duplicated from stripFrontmatter. If the frontmatter format changes or a bug is found in the parsing, both locations need to be updated consistently.

Consider extracting a shared helper like parseFrontmatter(raw: string): { frontmatter: string; body: string } | null and using it in both stripFrontmatter and getDocTitle. For example:

function parseFrontmatter(md: string): { frontmatter: string; body: string } | null {
  if (!md.startsWith('---')) return null;
  const end = md.indexOf('---', 3);
  if (end === -1) return null;
  return {
    frontmatter: md.slice(3, end),
    body: md.slice(end + 3).trim(),
  };
}

function stripFrontmatter(md: string): string {
  return parseFrontmatter(md)?.body ?? md;
}

export function getDocTitle(slug: DocSlug, language: string): string {
  const parsed = parseFrontmatter(getRawContent(slug, language));
  if (parsed) {
    const match = parsed.frontmatter.match(/title:\s*(.+)/);
    if (match) return match[1].trim();
  }
  return slug;
}

@@ -0,0 +1,187 @@
import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from 'react';

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.

Dead Code: useId is imported but never used anywhere in this component. Remove it from the import statement.

Suggestion:

Suggested change
import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from 'react';
import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react';

Comment on lines +126 to +147
const renderPromises = Array.from(mermaidBlocks).map(async (block) => {
const pre = block.parentElement;
if (!pre) return;
const code = block.textContent || '';
try {
const id = `mermaid-diagram-${crypto.randomUUID()}`;
const { svg } = await mermaid.render(id, code);
if (cancelled) return;
// Replace the <pre> with rendered SVG
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-rendered';
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (e) {
if (cancelled) return;
// If rendering fails, show the code block normally
(block as HTMLElement).style.display = 'block';
console.warn('[Mermaid] render failed:', e);
}
});

return () => { cancelled = true; };

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.

Async Handling Issue: The renderPromises array of promises is created but never awaited or handled with Promise.all. This means:

  1. The cleanup function (cancelled = true) executes synchronously before any mermaid.render() calls resolve, so the cancelled flag check inside the async callbacks may not work reliably if the effect re-runs quickly.
  2. If mermaid.render() throws outside the try/catch (e.g., during DOM insertion), it would become an unhandled promise rejection.
  3. There's no way to properly cancel in-flight renders on cleanup.

Use Promise.all and handle errors at the aggregate level, or at minimum ensure proper cleanup coordination.

Suggestion:

Suggested change
const renderPromises = Array.from(mermaidBlocks).map(async (block) => {
const pre = block.parentElement;
if (!pre) return;
const code = block.textContent || '';
try {
const id = `mermaid-diagram-${crypto.randomUUID()}`;
const { svg } = await mermaid.render(id, code);
if (cancelled) return;
// Replace the <pre> with rendered SVG
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-rendered';
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (e) {
if (cancelled) return;
// If rendering fails, show the code block normally
(block as HTMLElement).style.display = 'block';
console.warn('[Mermaid] render failed:', e);
}
});
return () => { cancelled = true; };
Promise.all(Array.from(mermaidBlocks).map(async (block) => {
const pre = block.parentElement;
if (!pre) return;
const code = block.textContent || '';
try {
const id = `mermaid-diagram-${crypto.randomUUID()}`;
const { svg } = await mermaid.render(id, code);
if (cancelled) return;
// Replace the <pre> with rendered SVG
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-rendered';
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (e) {
if (cancelled) return;
// If rendering fails, show the code block normally
(block as HTMLElement).style.display = 'block';
console.warn('[Mermaid] render failed:', e);
}
})).catch((e) => {
console.warn('[Mermaid] unexpected render error:', e);
});
return () => { cancelled = true; };

// Create copy button matching reference HTML
const btn = document.createElement('div');
btn.className = 'code-copy-btn';
btn.style.cssText = 'display:flex;flex-shrink:0;justify-content:flex-start;align-items:flex-start;flex-direction:column;padding-top:4px;padding-bottom:4px;cursor:pointer;';

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: The copy button uses extensive inline styles via cssText, but the parent <pre> element already has styling defined in docs-markdown.css (with display: flex; justify-content: space-between; align-items: center). These static styles should be moved to a CSS class in docs-markdown.css for consistency and maintainability. Only truly dynamic styles should remain inline.

Comment on lines +160 to +177
style={{
position: 'fixed',
top: 88,
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)',
color: 'rgba(255,255,255,0.85)',
padding: '5px 8px 5px 10px',
borderRadius: 6,
fontSize: 12,
fontWeight: 500,
pointerEvents: 'none',
opacity: toastVisible ? 1 : 0,
transition: 'opacity 0.15s ease',
zIndex: 9999,
backdropFilter: 'blur(8px)',
}}

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: The toast portal div uses extensive inline styles that are entirely static. These should be extracted into a CSS class (e.g., .copy-toast) in docs-markdown.css. The only dynamic property is opacity, which can be toggled via a class modifier (e.g., .copy-toast.visible). This improves maintainability and makes theme updates easier.

Comment thread pages/src/styles/docs-markdown.css Outdated
margin: 0 0 16px 0;
overflow-x: auto;
display: flex;
align-self: stretch;

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 align-self: stretch property only takes effect when the parent element is a flex or grid container. Since .docs-markdown does not have display: flex or display: grid, this property has no effect and can be removed to avoid confusion.

Comment on lines +6 to +10
export function generateHeadingId(text: string): string {
// Strip HTML tags first (from marked output), then strip markdown formatting chars
const plain = text.replace(/<[^>]+>/g, '').replace(/[`*_\[\]()]/g, '').trim();
return plain.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-').replace(/^-|-$/g, '');
}

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 duplicate ID issue: If a markdown document contains two headings with the same text (e.g., two ## Examples sections), this function will generate identical IDs for both. This results in:

  1. Invalid HTML (duplicate id attributes)
  2. Broken anchor navigation — clicking a TOC entry may scroll to the wrong heading
  3. IntersectionObserver tracking incorrect active heading

Consider adding deduplication logic by appending a suffix (e.g., -1, -2) when an ID has already been generated. Since this function is used in both extractHeadings and MarkdownRenderer, you'd need a stateful approach or pass a counter/map to ensure consistency between the two call sites.

hezheng-max added a commit to hezheng-max/open-code-review that referenced this pull request Jul 3, 2026
- Remove @types/dompurify (dompurify v3 ships built-in types)
- Fix 'QuickStart' → 'Quick Start' for consistency with navbar
- Extract parseFrontmatter helper to eliminate DRY violation
- Remove unused useId import from MarkdownRenderer
- Wrap mermaid renders with Promise.all and catch handler
- Move copy button inline styles to CSS class (.code-copy-btn)
- Move toast inline styles to CSS class (.copy-toast)
- Remove ineffective align-self: stretch from pre
- Add createHeadingIdGenerator for duplicate heading ID deduplication
@hezheng-max hezheng-max force-pushed the feat/docs-page-search branch from 19624af to 364cdc1 Compare July 3, 2026 08:20
…support

- Add DocsPage with full-text search modal and keyboard shortcuts
- Add MarkdownRenderer with DOMPurify sanitization and mermaid support
- Add bilingual docs content (en/zh) for all sections
- Add shared headingId utility with deduplication for consistent TOC anchors
- Add search keyboard hints with i18n support
- Update Navbar with Docs navigation link
- Configure webpack for markdown imports
- Extract parseFrontmatter helper to eliminate DRY violation
- Move copy button and toast inline styles to CSS classes
- Adjust HeroSection terminal section height and bottom padding
@hezheng-max hezheng-max force-pushed the feat/docs-page-search branch from 364cdc1 to ddd0eb7 Compare July 3, 2026 12:42
@hezheng-max hezheng-max changed the title fix: remove extra blank line in HeroSection terminal block feat(pages): add Docs page with search, markdown rendering, and i18n support Jul 3, 2026
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