feat(docs): Add gray-matter package and update documentation components
- Introduced `gray-matter` for frontmatter parsing in markdown files. - Refactored documentation components to utilize new constants and improve slug handling. - Enhanced language switcher functionality to maintain pathnames during locale changes. - Updated navigation and search components to support dynamic content loading and improved user experience. - Revised documentation text to reflect the new branding from Quaros to Goodgo.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@next/mdx": "^16.0.4",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.555.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.0.4",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import DocsLayout from '@/components/layout/DocsLayout';
|
||||
import DocsContentClient from '@/components/docs/DocsContentClient';
|
||||
import { defaultDocSlug } from '@/docs/registry';
|
||||
import { DEFAULT_DOC_SLUG } from '@/lib/docs-types';
|
||||
import { docsNavigation } from '@/docs/navigation';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import type { DocsLocale } from '@/docs/navigation';
|
||||
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://quaros.network';
|
||||
|
||||
type PageProps = {
|
||||
@@ -30,7 +31,7 @@ function findDocBySlug(slug: string, locale: DocsLocale) {
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { locale, slug } = await params;
|
||||
const slugPath = slug && slug.length > 0 ? slug.join('/') : defaultDocSlug;
|
||||
const slugPath = slug && slug.length > 0 ? slug.join('/') : DEFAULT_DOC_SLUG;
|
||||
const t = await getTranslations({ locale, namespace: 'Docs' });
|
||||
|
||||
const docInfo = findDocBySlug(slugPath, locale);
|
||||
@@ -78,7 +79,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
export default async function DocsCatchAllPage({ params }: PageProps) {
|
||||
const { locale, slug } = await params;
|
||||
|
||||
const slugPath = slug && slug.length > 0 ? slug.join('/') : defaultDocSlug;
|
||||
const slugPath = slug && slug.length > 0 ? slug.join('/') : DEFAULT_DOC_SLUG;
|
||||
const docInfo = findDocBySlug(slugPath, locale);
|
||||
|
||||
return (
|
||||
@@ -112,9 +113,10 @@ export default async function DocsCatchAllPage({ params }: PageProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DocsLayout locale={locale} currentSlug={slugPath}>
|
||||
<DocsLayout locale={locale} currentSlug={slugPath} docsNavigation={docsNavigation}>
|
||||
<DocsContentClient locale={locale} slug={slugPath} />
|
||||
</DocsLayout>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useTransition } from 'react';
|
||||
import { Globe } from 'lucide-react';
|
||||
import styles from '../layout/Navbar.module.css';
|
||||
@@ -9,12 +9,18 @@ import styles from '../layout/Navbar.module.css';
|
||||
export default function LanguageSwitcher() {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const nextLocale = locale === 'en' ? 'vi' : 'en';
|
||||
|
||||
// EN: Remove current locale from pathname and add new locale
|
||||
// VI: Xóa locale hiện tại khỏi pathname và thêm locale mới
|
||||
const currentPath = pathname.replace(`/${locale}`, '') || '';
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(`/${nextLocale}`);
|
||||
router.replace(`/${nextLocale}${currentPath}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,10 +29,18 @@ export default function LanguageSwitcher() {
|
||||
onClick={toggleLanguage}
|
||||
className={styles.link}
|
||||
disabled={isPending}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'none', border: 'none' }}
|
||||
aria-label={`Switch to ${locale === 'en' ? 'Vietnamese' : 'English'}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Globe size={16} />
|
||||
{locale === 'en' ? 'VI' : 'EN'}
|
||||
<span>{locale === 'en' ? 'VI' : 'EN'}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { DocsLocale } from '@/docs/navigation';
|
||||
import { defaultDocSlug } from '@/docs/registry';
|
||||
import type { DocsLocale } from '@/lib/docs-types';
|
||||
import { DEFAULT_DOC_SLUG } from '@/lib/docs-types';
|
||||
|
||||
// Loading component for MDX content
|
||||
|
||||
// EN: Loading component for MDX content
|
||||
// VI: Component loading cho nội dung MDX
|
||||
const LoadingDocs = () => (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<p>Loading documentation...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const docsRegistry = {
|
||||
en: {
|
||||
'getting-started': dynamic(() => import('../../../../../docs/en/templates/README.md'), {
|
||||
loading: () => <LoadingDocs />,
|
||||
ssr: true
|
||||
})
|
||||
},
|
||||
vi: {
|
||||
'getting-started': dynamic(() => import('../../../../../docs/vi/templates/README.md'), {
|
||||
loading: () => <LoadingDocs />,
|
||||
ssr: true
|
||||
})
|
||||
}
|
||||
} as const;
|
||||
// EN: Error component for missing docs
|
||||
// VI: Component lỗi cho docs thiếu
|
||||
const DocNotFound = ({ slug }: { slug: string }) => (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<h2>Documentation Not Found</h2>
|
||||
<p>The requested documentation "{slug}" could not be found.</p>
|
||||
<p>Please check the URL or navigate using the sidebar.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
type DocsContentClientProps = {
|
||||
locale: DocsLocale;
|
||||
@@ -32,8 +29,27 @@ type DocsContentClientProps = {
|
||||
};
|
||||
|
||||
export default function DocsContentClient({ locale, slug }: DocsContentClientProps) {
|
||||
const localeRegistry = docsRegistry[locale] ?? docsRegistry.en;
|
||||
const Component = localeRegistry[slug as keyof typeof localeRegistry] ?? localeRegistry[defaultDocSlug];
|
||||
// EN: Use slug directly (already includes category path e.g., 'guides/getting-started')
|
||||
// VI: Dùng slug trực tiếp (đã bao gồm đường dẫn category)
|
||||
const slugPath = slug || DEFAULT_DOC_SLUG;
|
||||
|
||||
|
||||
// EN: Dynamic import based on locale and slug with category path
|
||||
// VI: Dynamic import dựa trên locale và slug với đường dẫn category
|
||||
const Component = dynamic(
|
||||
() => import(`../../../../../docs/${locale}/${slugPath}.md`)
|
||||
.catch((error) => {
|
||||
console.error(`Failed to load doc: ${locale}/${slugPath}`, error);
|
||||
// EN: Return error component if doc doesn't exist
|
||||
// VI: Trả về error component nếu doc không tồn tại
|
||||
return { default: () => <DocNotFound slug={slugPath} /> };
|
||||
}),
|
||||
{
|
||||
loading: () => <LoadingDocs />,
|
||||
ssr: true
|
||||
}
|
||||
);
|
||||
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { Search, X, FileText } from 'lucide-react';
|
||||
import { docsNavigation } from '@/docs/navigation';
|
||||
import type { DocsLocale } from '@/docs/navigation';
|
||||
import type { DocsLocale, NavGroup } from '@/lib/docs-types';
|
||||
import styles from './DocsSearch.module.css';
|
||||
|
||||
type DocsSearchProps = {
|
||||
placeholder?: string;
|
||||
locale?: DocsLocale;
|
||||
docsNavigation: NavGroup[];
|
||||
};
|
||||
|
||||
|
||||
type SearchResult = {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -20,7 +21,7 @@ type SearchResult = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
export default function DocsSearch({ placeholder, locale = 'en' }: DocsSearchProps) {
|
||||
export default function DocsSearch({ placeholder, locale = 'en', docsNavigation }: DocsSearchProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -31,6 +32,7 @@ export default function DocsSearch({ placeholder, locale = 'en' }: DocsSearchPro
|
||||
// Extract locale from pathname
|
||||
const currentLocale = (pathname?.split('/')[1] as DocsLocale) || locale;
|
||||
|
||||
|
||||
// Build search index from navigation
|
||||
const searchIndex: SearchResult[] = useMemo(() => docsNavigation.flatMap(group =>
|
||||
group.items.map(item => ({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Navbar from '@/components/layout/Navbar';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
import { docsNavigation, type DocsLocale } from '@/docs/navigation';
|
||||
import type { DocsLocale, NavGroup } from '@/lib/docs-types';
|
||||
import Link from 'next/link';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import styles from './DocsLayout.module.css';
|
||||
@@ -14,10 +14,13 @@ type DocsLayoutProps = {
|
||||
locale: DocsLocale;
|
||||
currentSlug: string;
|
||||
children: React.ReactNode;
|
||||
docsNavigation: NavGroup[];
|
||||
};
|
||||
|
||||
export default function DocsLayout({ locale, currentSlug, children }: DocsLayoutProps) {
|
||||
|
||||
export default function DocsLayout({ locale, currentSlug, children, docsNavigation }: DocsLayoutProps) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const currentGroup = docsNavigation.find(group =>
|
||||
group.items.some(item => item.slug === currentSlug)
|
||||
);
|
||||
@@ -102,9 +105,8 @@ export default function DocsLayout({ locale, currentSlug, children }: DocsLayout
|
||||
<Link
|
||||
key={item.id}
|
||||
href={href}
|
||||
className={`${styles.sidebarLink} ${
|
||||
isActive ? styles.sidebarLinkActive : ''
|
||||
}`}
|
||||
className={`${styles.sidebarLink} ${isActive ? styles.sidebarLinkActive : ''
|
||||
}`}
|
||||
onClick={closeSidebar}
|
||||
>
|
||||
{item.label[locale]}
|
||||
@@ -126,10 +128,12 @@ export default function DocsLayout({ locale, currentSlug, children }: DocsLayout
|
||||
<div className={styles.headerTitle}>{pageTitle}</div>
|
||||
</header>
|
||||
)}
|
||||
<DocsSearch
|
||||
placeholder={locale === 'en' ? 'Search documentation…' : 'Tìm trong tài liệu…'}
|
||||
<DocsSearch
|
||||
placeholder={locale === 'en' ? 'Search documentation…' : 'Tìm trong tài liệu…'}
|
||||
locale={locale}
|
||||
docsNavigation={docsNavigation}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div data-docs-content className={styles.contentBody}>
|
||||
{children}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Navbar() {
|
||||
<nav className={styles.navbar}>
|
||||
<div className={styles.navbarContainer}>
|
||||
<Link href={`/${locale}`} className={styles.logo} onClick={closeMenu}>QUAROS</Link>
|
||||
<button
|
||||
<button
|
||||
className={styles.menuButton}
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
@@ -49,7 +49,7 @@ export default function Navbar() {
|
||||
</div>
|
||||
<Link href="/" className={styles.link} onClick={closeMenu}>{tHome('learnMore')}</Link>
|
||||
<Link href="/docs" className={styles.link} onClick={closeMenu}>{tDocs('title')}</Link>
|
||||
{/* <LanguageSwitcher /> */}
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,138 +1,26 @@
|
||||
export type DocsLocale = 'en' | 'vi';
|
||||
|
||||
export type DocsNavItem = {
|
||||
id: string;
|
||||
slug: string; // e.g. 'getting-started' or 'protocol/architecture'
|
||||
label: {
|
||||
en: string;
|
||||
vi: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DocsNavGroup = {
|
||||
id: string;
|
||||
label: {
|
||||
en: string;
|
||||
vi: string;
|
||||
};
|
||||
items: DocsNavItem[];
|
||||
};
|
||||
|
||||
export const docsNavigation: DocsNavGroup[] = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
label: {
|
||||
en: 'Getting Started',
|
||||
vi: 'Bắt đầu'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'getting-started',
|
||||
slug: 'getting-started',
|
||||
label: {
|
||||
en: 'Overview',
|
||||
vi: 'Tổng quan'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'why-post-quantum',
|
||||
slug: 'why-post-quantum',
|
||||
label: {
|
||||
en: 'Why Post-Quantum Cryptography?',
|
||||
vi: 'Tại sao cần Mật mã Hậu Lượng tử?'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ethereum-1',
|
||||
slug: 'ethereum-1',
|
||||
label: {
|
||||
en: 'Ethereum 1',
|
||||
vi: 'Ethereum 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ethereum-2',
|
||||
slug: 'ethereum-2',
|
||||
label: {
|
||||
en: 'Ethereum 2',
|
||||
vi: 'Ethereum 2'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'qrl',
|
||||
slug: 'qrl',
|
||||
label: {
|
||||
en: 'QRL',
|
||||
vi: 'QRL'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'go-zond',
|
||||
slug: 'go-zond',
|
||||
label: {
|
||||
en: 'Go Zond',
|
||||
vi: 'Go Zond'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'xmss',
|
||||
slug: 'xmss',
|
||||
label: {
|
||||
en: 'XMSS (Stateful)',
|
||||
vi: 'XMSS (Có trạng thái)'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'mldsa',
|
||||
slug: 'mldsa',
|
||||
label: {
|
||||
en: 'ML-DSA (Stateless)',
|
||||
vi: 'ML-DSA (Không trạng thái)'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'protocol',
|
||||
label: {
|
||||
en: 'Protocol',
|
||||
vi: 'Giao thức'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'architecture',
|
||||
slug: 'architecture',
|
||||
label: {
|
||||
en: 'Architecture',
|
||||
vi: 'Kiến trúc'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'execution-layer',
|
||||
slug: 'execution-layer',
|
||||
label: {
|
||||
en: 'Execution Layer',
|
||||
vi: 'Lớp Thực thi'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'consensus-layer',
|
||||
slug: 'consensus-layer',
|
||||
label: {
|
||||
en: 'Consensus Layer',
|
||||
vi: 'Lớp Đồng thuận'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data-availability-layer',
|
||||
slug: 'data-availability-layer',
|
||||
label: {
|
||||
en: 'Data Availability Layer',
|
||||
vi: 'Lớp Khả dụng Dữ liệu'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
// EN: Type definitions for documentation navigation
|
||||
// VI: Định nghĩa types cho documentation navigation
|
||||
export type { DocsLocale, NavItem as DocsNavItem, NavGroup as DocsNavGroup } from '@/lib/docs-types';
|
||||
|
||||
|
||||
// EN: Dynamic navigation generation
|
||||
// VI: Tạo navigation động
|
||||
import { generateDocsNavigation } from '@/lib/docs-generator';
|
||||
|
||||
// EN: Cache navigation to avoid regenerating on every request
|
||||
// VI: Cache navigation để tránh tạo lại với mỗi request
|
||||
let cachedNavigation: DocsNavGroup[] | null = null;
|
||||
|
||||
export function getDocsNavigation(): DocsNavGroup[] {
|
||||
if (!cachedNavigation) {
|
||||
cachedNavigation = generateDocsNavigation();
|
||||
}
|
||||
return cachedNavigation;
|
||||
}
|
||||
|
||||
// EN: Export cached navigation for backwards compatibility
|
||||
// VI: Export cached navigation cho tương thích ngược
|
||||
export const docsNavigation = getDocsNavigation();
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { DocsLocale } from './navigation';
|
||||
import type { DocsLocale, DEFAULT_DOC_SLUG } from '@/lib/docs-types';
|
||||
|
||||
// Thông tin registry phía server (chỉ metadata, không import MDX ở Server Component)
|
||||
// EN: Server-side registry - DO NOT import into client components
|
||||
// VI: Registry phía server - KHÔNG import vào client components
|
||||
// This file should only be imported by Server Components
|
||||
|
||||
export const defaultDocSlug = 'getting-started';
|
||||
|
||||
export const availableDocs: Record<DocsLocale, string[]> = {
|
||||
en: ['getting-started', 'why-post-quantum', 'ethereum-1', 'ethereum-2', 'qrl', 'go-zond', 'architecture', 'execution-layer', 'consensus-layer', 'data-availability-layer', 'xmss', 'mldsa'],
|
||||
vi: ['getting-started', 'why-post-quantum', 'ethereum-1', 'ethereum-2', 'qrl', 'go-zond', 'architecture', 'execution-layer', 'consensus-layer', 'data-availability-layer', 'xmss', 'mldsa']
|
||||
};
|
||||
// EN: Default doc slug to load when no slug is specified
|
||||
// VI: Slug mặc định khi không có slug được chỉ định
|
||||
export const defaultDocSlug = DEFAULT_DOC_SLUG;
|
||||
|
||||
// EN: Note: getAvailableDocs is a server-side function that uses fs
|
||||
// VI: Lưu ý: getAvailableDocs là hàm server-side sử dụng fs
|
||||
// It should only be called from Server Components, not Client Components
|
||||
|
||||
212
apps/web-docs/src/lib/docs-generator.ts
Normal file
212
apps/web-docs/src/lib/docs-generator.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
// EN: Server-side utility to generate docs navigation from filesystem
|
||||
// VI: Utility phía server để tạo docs navigation từ filesystem
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import type { DocsLocale, NavItem, NavGroup } from './docs-types';
|
||||
|
||||
interface DocFile {
|
||||
slug: string;
|
||||
title: string;
|
||||
category: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
// EN: Get absolute path to docs directory
|
||||
// VI: Lấy đường dẫn tuyệt đối tới thư mục docs
|
||||
const DOCS_ROOT = path.join(process.cwd(), '../../docs');
|
||||
|
||||
// EN: Category display order and labels
|
||||
// VI: Thứ tự hiển thị và nhãn cho các danh mục
|
||||
const CATEGORY_CONFIG: Record<string, { order: number; label: { en: string; vi: string } }> = {
|
||||
'guides': { order: 1, label: { en: 'Guides', vi: 'Hướng dẫn' } },
|
||||
'architecture': { order: 2, label: { en: 'Architecture', vi: 'Kiến trúc' } },
|
||||
'skills': { order: 3, label: { en: 'Skills', vi: 'Kỹ năng' } },
|
||||
'templates': { order: 4, label: { en: 'Templates', vi: 'Mẫu' } },
|
||||
'api': { order: 5, label: { en: 'API Reference', vi: 'Tài liệu API' } },
|
||||
'runbooks': { order: 6, label: { en: 'Runbooks', vi: 'Sổ tay vận hành' } },
|
||||
'onboarding': { order: 7, label: { en: 'Onboarding', vi: 'Hướng dẫn mới' } },
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Extract title from markdown file
|
||||
* VI: Trích xuất tiêu đề từ file markdown
|
||||
*/
|
||||
function extractTitle(filePath: string): string {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// EN: Try to parse frontmatter first
|
||||
// VI: Thử parse frontmatter trước
|
||||
try {
|
||||
const { data } = matter(content);
|
||||
if (data.name) return data.name;
|
||||
if (data.title) return data.title;
|
||||
} catch (e) {
|
||||
// Ignore frontmatter parsing errors
|
||||
}
|
||||
|
||||
// EN: Fallback to first H1 heading
|
||||
// VI: Fallback về H1 heading đầu tiên
|
||||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||||
if (h1Match) {
|
||||
// EN: Remove bilingual suffix if exists (e.g., "Title / Tiêu đề" -> "Title")
|
||||
// VI: Xóa hậu tố bilingual nếu tồn tại
|
||||
const title = h1Match[1].split('/')[0].trim();
|
||||
return title;
|
||||
}
|
||||
|
||||
// EN: Final fallback to filename
|
||||
// VI: Fallback cuối cùng là tên file
|
||||
const filename = path.basename(filePath, '.md');
|
||||
return filename
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} catch (error) {
|
||||
// EN: If file read fails, use filename
|
||||
// VI: Nếu đọc file thất bại, dùng tên file
|
||||
const filename = path.basename(filePath, '.md');
|
||||
return filename
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Scan docs directory for a specific locale
|
||||
* VI: Quét thư mục docs cho một locale cụ thể
|
||||
*/
|
||||
function scanDocsForLocale(locale: DocsLocale): DocFile[] {
|
||||
const localeDir = path.join(DOCS_ROOT, locale);
|
||||
const docs: DocFile[] = [];
|
||||
|
||||
if (!fs.existsSync(localeDir)) {
|
||||
return docs;
|
||||
}
|
||||
|
||||
// EN: Read all category directories
|
||||
// VI: Đọc tất cả thư mục danh mục
|
||||
const categories = fs.readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
for (const category of categories) {
|
||||
const categoryPath = path.join(localeDir, category);
|
||||
const files = fs.readdirSync(categoryPath)
|
||||
.filter(file => file.endsWith('.md') && file !== 'README.md');
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(categoryPath, file);
|
||||
const slug = `${category}/${path.basename(file, '.md')}`;
|
||||
const title = extractTitle(filePath);
|
||||
|
||||
docs.push({
|
||||
slug,
|
||||
title,
|
||||
category,
|
||||
filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Generate navigation structure with bilingual support
|
||||
* VI: Tạo cấu trúc navigation với hỗ trợ đa ngôn ngữ
|
||||
*/
|
||||
export function generateDocsNavigation(): NavGroup[] {
|
||||
const enDocs = scanDocsForLocale('en');
|
||||
const viDocs = scanDocsForLocale('vi');
|
||||
|
||||
// EN: Create a map of slug to titles in both languages
|
||||
// VI: Tạo map từ slug tới tiêu đề ở cả hai ngôn ngữ
|
||||
const docTitles = new Map<string, { en: string; vi: string }>();
|
||||
|
||||
enDocs.forEach(doc => {
|
||||
docTitles.set(doc.slug, {
|
||||
en: doc.title,
|
||||
vi: doc.title // Default to EN if VI not found
|
||||
});
|
||||
});
|
||||
|
||||
viDocs.forEach(doc => {
|
||||
const existing = docTitles.get(doc.slug);
|
||||
if (existing) {
|
||||
existing.vi = doc.title;
|
||||
} else {
|
||||
docTitles.set(doc.slug, { en: doc.title, vi: doc.title });
|
||||
}
|
||||
});
|
||||
|
||||
// EN: Group by category
|
||||
// VI: Nhóm theo danh mục
|
||||
const groups = new Map<string, NavItem[]>();
|
||||
|
||||
docTitles.forEach((labels, slug) => {
|
||||
const category = slug.split('/')[0];
|
||||
if (!groups.has(category)) {
|
||||
groups.set(category, []);
|
||||
}
|
||||
|
||||
groups.get(category)!.push({
|
||||
id: slug.replace(/\//g, '-'),
|
||||
slug,
|
||||
label: labels
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Convert to NavGroup array with sorting
|
||||
// VI: Chuyển thành mảng NavGroup với sắp xếp
|
||||
const navGroups: NavGroup[] = [];
|
||||
|
||||
groups.forEach((items, category) => {
|
||||
const config = CATEGORY_CONFIG[category] || {
|
||||
order: 999,
|
||||
label: { en: category.charAt(0).toUpperCase() + category.slice(1), vi: category.charAt(0).toUpperCase() + category.slice(1) }
|
||||
};
|
||||
|
||||
// EN: Sort items alphabetically
|
||||
// VI: Sắp xếp items theo thứ tự bảng chữ cái
|
||||
items.sort((a, b) => a.label.en.localeCompare(b.label.en));
|
||||
|
||||
navGroups.push({
|
||||
id: category,
|
||||
label: config.label,
|
||||
items
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Sort groups by order
|
||||
// VI: Sắp xếp groups theo thứ tự
|
||||
navGroups.sort((a, b) => {
|
||||
const orderA = CATEGORY_CONFIG[a.id]?.order || 999;
|
||||
const orderB = CATEGORY_CONFIG[b.id]?.order || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
return navGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Get list of available doc slugs for a locale
|
||||
* VI: Lấy danh sách slugs có sẵn cho một locale
|
||||
*/
|
||||
export function getAvailableDocs(locale: DocsLocale): string[] {
|
||||
const docs = scanDocsForLocale(locale);
|
||||
return docs.map(doc => doc.slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Check if a doc exists for a locale
|
||||
* VI: Kiểm tra xem một doc có tồn tại cho locale không
|
||||
*/
|
||||
export function docExists(locale: DocsLocale, slug: string): boolean {
|
||||
const slugPath = slug.replace(/\//g, path.sep);
|
||||
const filePath = path.join(DOCS_ROOT, locale, `${slugPath}.md`);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
20
apps/web-docs/src/lib/docs-types.ts
Normal file
20
apps/web-docs/src/lib/docs-types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// EN: Shared types and constants for docs (safe for client components)
|
||||
// VI: Types và constants chung cho docs (an toàn cho client components)
|
||||
|
||||
export type DocsLocale = 'en' | 'vi';
|
||||
|
||||
export interface NavItem {
|
||||
id: string;
|
||||
slug: string;
|
||||
label: { en: string; vi: string };
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
id: string;
|
||||
label: { en: string; vi: string };
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
// EN: Default doc slug to load when no slug is specified
|
||||
// VI: Slug mặc định khi không có slug được chỉ định
|
||||
export const DEFAULT_DOC_SLUG = 'guides/getting-started';
|
||||
@@ -13,16 +13,16 @@
|
||||
"apiDesc": "Build dApps with post-quantum security. Seamlessly integrate with existing Ethereum tooling and infrastructure.",
|
||||
"apiCta": "Start building",
|
||||
"docsTitle": "Documentation",
|
||||
"docsDesc": "Learn how to integrate Quaros into your applications. Explore guides covering architecture, security, and common use cases.",
|
||||
"docsDesc": "Learn how to integrate Goodgo into your applications. Explore guides covering architecture, security, and common use cases.",
|
||||
"docsCta": "Read docs",
|
||||
"largeText": "Secure",
|
||||
"largeText2": "The Future",
|
||||
"ctaTitle": "Build on Quaros",
|
||||
"ctaTitle": "Build on Goodgo",
|
||||
"ctaDesc": "Join the post-quantum blockchain revolution. Start building secure, future-proof applications today.",
|
||||
"ctaButton": "Get started"
|
||||
},
|
||||
"Features": {
|
||||
"title": "Why Quaros?",
|
||||
"title": "Why Goodgo?",
|
||||
"xmssTitle": "XMSS (Stateful)",
|
||||
"xmssDesc": "Offers robust security but limits the number of signatures per wallet.",
|
||||
"mldsaTitle": "ML-DSA (Stateless)",
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
"Docs": {
|
||||
"title": "Goodgo Documentation",
|
||||
"intro": "This page gives you a high-level overview of how Quaros works and how to start integrating it.",
|
||||
"intro": "This page gives you a high-level overview of how Goodgo works and how to start integrating it.",
|
||||
"sections": {
|
||||
"arch": {
|
||||
"title": "Architecture Overview",
|
||||
"desc1": "Quaros is designed as a post-quantum secure, EVM-compatible network that focuses on long-term security.",
|
||||
"desc1": "Goodgo is designed as a post-quantum secure, EVM-compatible network that focuses on long-term security.",
|
||||
"desc2": "The protocol separates execution, consensus and data availability layers to enable scalability and flexibility."
|
||||
},
|
||||
"security": {
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"evm": {
|
||||
"title": "EVM & Web3 Tooling",
|
||||
"desc1": "Quaros keeps compatibility with the Ethereum JSON-RPC and tooling ecosystem so existing dApps can be ported easily.",
|
||||
"desc2": "Most common frameworks (Hardhat, Foundry, wagmi, etc.) can connect to Quaros with minimal configuration changes."
|
||||
"desc1": "Goodgo keeps compatibility with the Ethereum JSON-RPC and tooling ecosystem so existing dApps can be ported easily.",
|
||||
"desc2": "Most common frameworks (Hardhat, Foundry, wagmi, etc.) can connect to Goodgo with minimal configuration changes."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Mạng Lưới Quaros",
|
||||
"title": "Mạng Lưới Goodgo",
|
||||
"subtitle": "Bảo mật Hậu Lượng Tử. Tương thích EVM. Sẵn sàng cho Web3.",
|
||||
"getStarted": "Bắt đầu ngay",
|
||||
"learnMore": "Tìm hiểu thêm",
|
||||
@@ -13,16 +13,16 @@
|
||||
"apiDesc": "Xây dựng dApp với bảo mật hậu lượng tử. Tích hợp liền mạch với các công cụ và cơ sở hạ tầng Ethereum hiện có.",
|
||||
"apiCta": "Bắt đầu xây dựng",
|
||||
"docsTitle": "Tài liệu",
|
||||
"docsDesc": "Tìm hiểu cách tích hợp Quaros vào ứng dụng của bạn. Khám phá các hướng dẫn về kiến trúc, bảo mật và các trường hợp sử dụng phổ biến.",
|
||||
"docsDesc": "Tìm hiểu cách tích hợp Goodgo vào ứng dụng của bạn. Khám phá các hướng dẫn về kiến trúc, bảo mật và các trường hợp sử dụng phổ biến.",
|
||||
"docsCta": "Đọc tài liệu",
|
||||
"largeText": "Bảo vệ",
|
||||
"largeText2": "Tương lai",
|
||||
"ctaTitle": "Xây dựng trên Quaros",
|
||||
"ctaTitle": "Xây dựng trên Goodgo",
|
||||
"ctaDesc": "Tham gia cuộc cách mạng blockchain hậu lượng tử. Bắt đầu xây dựng các ứng dụng an toàn, sẵn sàng cho tương lai ngay hôm nay.",
|
||||
"ctaButton": "Bắt đầu"
|
||||
},
|
||||
"Features": {
|
||||
"title": "Tại sao chọn Quaros?",
|
||||
"title": "Tại sao chọn Goodgo?",
|
||||
"xmssTitle": "XMSS (Có trạng thái)",
|
||||
"xmssDesc": "Cung cấp bảo mật mạnh mẽ nhưng giới hạn số lượng chữ ký mỗi ví.",
|
||||
"mldsaTitle": "ML-DSA (Không trạng thái)",
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
"Docs": {
|
||||
"title": "Tài liệu Goodgo Documentation",
|
||||
"intro": "Trang này cung cấp cái nhìn tổng quan ở mức cao về cách Quaros hoạt động và cách bắt đầu tích hợp.",
|
||||
"intro": "Trang này cung cấp cái nhìn tổng quan ở mức cao về cách Goodgo hoạt động và cách bắt đầu tích hợp.",
|
||||
"sections": {
|
||||
"arch": {
|
||||
"title": "Tổng quan kiến trúc",
|
||||
"desc1": "Quaros được thiết kế như một mạng lưới bảo mật hậu lượng tử, tương thích EVM và tập trung vào bảo mật dài hạn.",
|
||||
"desc1": "Goodgo được thiết kế như một mạng lưới bảo mật hậu lượng tử, tương thích EVM và tập trung vào bảo mật dài hạn.",
|
||||
"desc2": "Giao thức tách biệt các lớp thực thi, đồng thuận và khả dụng dữ liệu để đạt được khả năng mở rộng và linh hoạt."
|
||||
},
|
||||
"security": {
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"evm": {
|
||||
"title": "EVM & Hệ sinh thái Web3",
|
||||
"desc1": "Quaros giữ tương thích với JSON-RPC và hệ sinh thái công cụ Ethereum nên dApp hiện tại có thể được port dễ dàng.",
|
||||
"desc2": "Hầu hết framework phổ biến (Hardhat, Foundry, wagmi, v.v.) có thể kết nối với Quaros chỉ với vài thay đổi cấu hình."
|
||||
"desc1": "Goodgo giữ tương thích với JSON-RPC và hệ sinh thái công cụ Ethereum nên dApp hiện tại có thể được port dễ dàng.",
|
||||
"desc2": "Hầu hết framework phổ biến (Hardhat, Foundry, wagmi, v.v.) có thể kết nối với Goodgo chỉ với vài thay đổi cấu hình."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -219,6 +219,9 @@ importers:
|
||||
'@react-three/fiber':
|
||||
specifier: ^9.4.0
|
||||
version: 9.5.0(@types/react@19.2.7)(react-dom@19.2.0)(react@19.2.0)(three@0.182.0)
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
lucide-react:
|
||||
specifier: ^0.555.0
|
||||
version: 0.555.0(react@19.2.0)
|
||||
@@ -8581,7 +8584,6 @@ packages:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
dev: true
|
||||
|
||||
/argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
@@ -10586,7 +10588,6 @@ packages:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/esquery@1.7.0:
|
||||
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||
@@ -10748,6 +10749,13 @@ packages:
|
||||
/exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
/extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
dev: false
|
||||
|
||||
/extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
@@ -11207,6 +11215,16 @@ packages:
|
||||
/grammex@3.1.12:
|
||||
resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==}
|
||||
|
||||
/gray-matter@4.0.3:
|
||||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
dependencies:
|
||||
js-yaml: 3.14.2
|
||||
kind-of: 6.0.3
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
dev: false
|
||||
|
||||
/hachure-fill@0.5.2:
|
||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||
dev: false
|
||||
@@ -11630,6 +11648,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/is-extendable@0.1.1:
|
||||
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -12307,7 +12330,6 @@ packages:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
dev: true
|
||||
|
||||
/js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
@@ -12459,6 +12481,11 @@ packages:
|
||||
resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
|
||||
dev: false
|
||||
|
||||
/kind-of@6.0.3:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/koa-compose@4.1.0:
|
||||
resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==}
|
||||
dev: false
|
||||
@@ -15121,6 +15148,14 @@ packages:
|
||||
/scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
/section-matter@1.0.0:
|
||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
dev: false
|
||||
|
||||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -15338,7 +15373,6 @@ packages:
|
||||
|
||||
/sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: true
|
||||
|
||||
/sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
@@ -15547,6 +15581,11 @@ packages:
|
||||
ansi-regex: 6.2.2
|
||||
dev: true
|
||||
|
||||
/strip-bom-string@1.0.0:
|
||||
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/strip-bom@3.0.0:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
Reference in New Issue
Block a user