feat(pages): add Docs page with search, markdown rendering, and i18n support#285
feat(pages): add Docs page with search, markdown rendering, and i18n support#285hezheng-max wants to merge 1 commit into
Conversation
| "@babel/preset-env": "^7.29.5", | ||
| "@babel/preset-react": "^7.23.5", | ||
| "@babel/preset-typescript": "^7.23.3", | ||
| "@types/dompurify": "^3.0.5", |
There was a problem hiding this comment.
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.
| 'docs.sidebar.gettingStarted': 'Getting Started', | ||
| 'docs.sidebar.userGuide': 'User Guide', | ||
| 'docs.sidebar.overview': 'Overview', | ||
| 'docs.sidebar.quickstart': 'QuickStart', |
There was a problem hiding this comment.
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:
| 'docs.sidebar.quickstart': 'QuickStart', | |
| 'docs.sidebar.quickstart': 'Quick Start', |
| 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; | ||
| } |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
Dead Code: useId is imported but never used anywhere in this component. Remove it from the import statement.
Suggestion:
| import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from 'react'; | |
| import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react'; |
| 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; }; |
There was a problem hiding this comment.
Async Handling Issue: The renderPromises array of promises is created but never awaited or handled with Promise.all. This means:
- The cleanup function (
cancelled = true) executes synchronously before anymermaid.render()calls resolve, so thecancelledflag check inside the async callbacks may not work reliably if the effect re-runs quickly. - If
mermaid.render()throws outside the try/catch (e.g., during DOM insertion), it would become an unhandled promise rejection. - 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:
| 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;'; |
There was a problem hiding this comment.
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.
| 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)', | ||
| }} |
There was a problem hiding this comment.
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.
| margin: 0 0 16px 0; | ||
| overflow-x: auto; | ||
| display: flex; | ||
| align-self: stretch; |
There was a problem hiding this comment.
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.
| 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, ''); | ||
| } |
There was a problem hiding this comment.
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:
- Invalid HTML (duplicate
idattributes) - Broken anchor navigation — clicking a TOC entry may scroll to the wrong heading
- 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.
- 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
19624af to
364cdc1
Compare
…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
364cdc1 to
ddd0eb7
Compare
Uh oh!
There was an error while loading. Please reload this page.