Skip to main content

Để xây dựng thành công trang Navigation

Xây dựng trang độc lập "Navigation.tsx" trong "Docusaurus+Material" thật khó thành công, cần rất nhiều thời gian và công sức! Xây dựng trang này nhằm show toàn bộ bài viết trong Website để có cái nhìn tổng quát về những chủ đề Website đề cập đến!

Tất cả nội dung chỉ xem ở đây, không cần duyệt qua nhiều Tab! Trang gồm 2 phần: "Sidebar" bên trái và bên phải là khung "content". Sidebar sẽ show toàn bộ đề mục bài viết trong Site, và khi click chuột vào đề mục cần xem thì nội dung tương ứng của đề mục này sẽ bật ra trong khung "content"> Vì vậy ta thoải mái tìm chủ đề cần đọc trong danh sách ở Sidebar, để xem nội dung hiển thị trong khung "content", không cần phải đi đâu cả!

Các bước thực hiện như sau:

1)Cài đặt "Gray-matter" bằng cách gõ vào Terminal:

npm install gray-matter

2)Tạo folder "plugins" ở thư mục gốc dự án, sau đó tạo file "plugins-docusaurus-plugin-generate-index.js" để xuất "static/indexData.json" và "docs/index.md" với code hoàn chỉnh sau:

const fs = require('fs');
const path = require('path');
const matter = require('gray-matter'); // Import gray-matter

// Hàm chuẩn hóa tên file (chuyển không dấu, thay khoảng trắng thành '-')
function normalizeFileName(fileName) {
    return fileName
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '') // Loại bỏ dấu
        .replace(/\s+/g, '-') // Thay khoảng trắng bằng dấu '-'
        .toLowerCase();
}

// Hàm trích xuất tiêu đề từ front-matter hoặc tiêu đề Markdown
function extractTitle(filePath) {
    try {
        const fileContent = fs.readFileSync(filePath, 'utf8');
        const { data, content } = matter(fileContent); // Phân tích front-matter và nội dung

        // Lấy tiêu đề từ front-matter nếu có
        if (data.title) {
            return data.title.trim();
        }

        // Nếu không có front-matter hoặc không có `title`, fallback về tiêu đề Markdown
        const markdownTitleMatch = content.match(/^#\s(.+)/);
        return markdownTitleMatch ? markdownTitleMatch[1].trim() : null;

    } catch (error) {
        console.error(`Lỗi khi đọc file: ${filePath}`, error);
        return null;
    }
}

// Hàm đọc toàn bộ nội dung (bỏ front-matter nếu có)
function extractFullContent(filePath) {
    try {
        const fileContent = fs.readFileSync(filePath, 'utf8');
        const { content } = matter(fileContent); // Phân tích và loại bỏ front-matter
        return content.trim();
    } catch (error) {
        console.error(`Lỗi khi đọc nội dung từ file: ${filePath}`, error);
        return 'Không thể đọc nội dung.';
    }
}

// Hàm tạo danh sách index và lưu toàn bộ nội dung
function generateIndex(dirPath, basePath = '', output = []) {
    const items = fs.readdirSync(dirPath, { withFileTypes: true });

    items.forEach((item) => {
        const fullPath = path.join(dirPath, item.name);
        if (item.isDirectory()) {
            // Nếu là thư mục, đệ quy xử lý
            generateIndex(fullPath, path.join(basePath, item.name), output);
        } else if (item.name.endsWith('.md') || item.name.endsWith('.mdx')) {
            const title = extractTitle(fullPath); // Lấy tiêu đề
            const content = extractFullContent(fullPath); // Lấy toàn bộ nội dung
            const label = title || normalizeFileName(item.name.replace(/\.mdx?$/, ''));
            const relativePath = path.join(basePath, item.name).replace(/\\/g, '/');

            // Lưu thông tin bài viết vào mảng đầu ra
            output.push({
                label,
                path: `/docs/${relativePath}`,
                content,
            });
        }
    });

    return output;
}

// Hàm chuyển danh sách index thành nội dung Markdown
function generateMarkdownContent(indexData) {
    const lines = ['# Index', ''];
    indexData.forEach((item) => {
        lines.push(`- [${item.label}](${item.path})`);
    });
    return lines.join('\n');
}

// Xuất plugin
module.exports = function (context, options) {
    return {
        name: 'generate-index-plugin',
        async loadContent() {
            const docsDir = path.join(context.siteDir, 'docs');
            const jsonOutputFile = path.join(context.siteDir, 'static', 'indexData.json'); // Lưu JSON vào thư mục tĩnh
            const markdownOutputFile = path.join(docsDir, 'index.md'); // Lưu file Markdown vào thư mục docs

            const indexData = generateIndex(docsDir);

            // Ghi dữ liệu JSON
            fs.writeFileSync(jsonOutputFile, JSON.stringify(indexData, null, 2));
            console.log('Index data JSON created at:', jsonOutputFile);

            // Ghi dữ liệu Markdown
            const markdownContent = generateMarkdownContent(indexData);
            fs.writeFileSync(markdownOutputFile, markdownContent);
            console.log('Index Markdown created at:', markdownOutputFile);
        },
    };
};

Nếu chỉ muốn xuất "static/indexData.json", không cần "docs/index.md" thì chỉ cần nhập code như sau:

const fs = require('fs');
const path = require('path');
const matter = require('gray-matter'); // Import gray-matter

// Hàm chuẩn hóa tên file (chuyển không dấu, thay khoảng trắng thành '-')
function normalizeFileName(fileName) {
    return fileName
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '') // Loại bỏ dấu
        .replace(/\s+/g, '-') // Thay khoảng trắng bằng dấu '-'
        .toLowerCase();
}

// Hàm trích xuất tiêu đề từ front-matter hoặc tiêu đề Markdown
function extractTitle(filePath) {
    try {
        const fileContent = fs.readFileSync(filePath, 'utf8');
        const { data, content } = matter(fileContent); // Phân tích front-matter và nội dung

        // Lấy tiêu đề từ front-matter nếu có
        if (data.title) {
            return data.title.trim();
        }

        // Nếu không có front-matter hoặc không có `title`, fallback về tiêu đề Markdown
        const markdownTitleMatch = content.match(/^#\s(.+)/);
        return markdownTitleMatch ? markdownTitleMatch[1].trim() : null;

    } catch (error) {
        console.error(`Lỗi khi đọc file: ${filePath}`, error);
        return null;
    }
}

// Hàm đọc toàn bộ nội dung (bỏ front-matter nếu có)
function extractFullContent(filePath) {
    try {
        const fileContent = fs.readFileSync(filePath, 'utf8');
        const { content } = matter(fileContent); // Phân tích và loại bỏ front-matter
        return content.trim();
    } catch (error) {
        console.error(`Lỗi khi đọc nội dung từ file: ${filePath}`, error);
        return 'Không thể đọc nội dung.';
    }
}

// Hàm tạo danh sách index và lưu toàn bộ nội dung
function generateIndex(dirPath, basePath = '', output = []) {
    const items = fs.readdirSync(dirPath, { withFileTypes: true });

    items.forEach((item) => {
        const fullPath = path.join(dirPath, item.name);
        if (item.isDirectory()) {
            // Nếu là thư mục, đệ quy xử lý
            generateIndex(fullPath, path.join(basePath, item.name), output);
        } else if (item.name.endsWith('.md') || item.name.endsWith('.mdx')) {
            const title = extractTitle(fullPath); // Lấy tiêu đề
            const content = extractFullContent(fullPath); // Lấy toàn bộ nội dung
            const label = title || normalizeFileName(item.name.replace(/\.mdx?$/, ''));
            const relativePath = path.join(basePath, item.name).replace(/\\/g, '/');

            // Lưu thông tin bài viết vào mảng đầu ra
            output.push({
                label,
                path: `/docs/${relativePath}`,
                content,
            });
        }
    });

    return output;
}

// Xuất plugin
module.exports = function (context, options) {
    return {
        name: 'generate-index-plugin',
        async loadContent() {
            const docsDir = path.join(context.siteDir, 'docs');
            const outputFile = path.join(context.siteDir, 'static', 'indexData.json'); // Lưu JSON vào thư mục tĩnh

            const indexData = generateIndex(docsDir);

            fs.writeFileSync(outputFile, JSON.stringify(indexData, null, 2)); // Ghi dữ liệu vào file JSON
            console.log('Index data JSON created at:', outputFile);
        },
    };
};

3)Tạo trang: "src/pages/navigation.tsx" Trong trang này cần thêm thư viện "marked" để chuyển đổi Markdown sang HTML, nên cần cài đặt "marked", gõ lệnh ở Terminal:

npm install marked

Sau đó nhập code này vào "navigation.tsx":

import React, { useEffect, useState } from 'react';
import Layout from '@theme/Layout';
import { marked } from 'marked'; // Thư viện để xử lý Markdown
import styles from './navigation.module.css';

interface Article {
  label: string;
  path: string;
  content: string; // Nội dung Markdown
}

const NavigationPage = () => {
  const [articles, setArticles] = useState<Article[]>([]);
  const [activeContent, setActiveContent] = useState<string>('');

  useEffect(() => {
    fetch('/indexData.json')
      .then((response) => response.json())
      .then((data: Article[]) => {
        setArticles(data);
        if (data.length > 0) {
          setActiveContent(data[0].content);
        }
      })
      .catch((error) => {
        console.error('Error fetching index data:', error);
      });
  }, []);

  const handleLinkClick = (content: string) => {
    setActiveContent(content);
  };

  return (
    <Layout title="Navigation Page">
      <div className={styles.container}>
        <div className={styles.sidebar}>
          <h2>Sidebar</h2>
          <ul>
            {articles.map((article, index) => (
              <li key={index}>
                <a
                  href="#"
                  onClick={(e) => {
                    e.preventDefault();
                    handleLinkClick(article.content);
                  }}
                >
                  {article.label}
                </a>
              </li>
            ))}
          </ul>
        </div>

        <div className={styles.content}>
          <h1>Content</h1>
          <div dangerouslySetInnerHTML={{ __html: marked(activeContent) }} />
        </div>
      </div>
    </Layout>
  );
};

export default NavigationPage;

4)Tạo file "src/pages/navigation.module.css" để cải thiện hiển thị:

.container {
  display: flex;
}

.sidebar {
  width: 25%;
  padding: 20px;
  border-right: 1px solid #ddd;
}

.content {
  width: 75%;
  padding: 20px;
  overflow-y: auto; /* Thêm thanh cuộn nếu nội dung quá dài */
}

.content h1,
.content h2,
.content h3 {
  font-family: 'Arial', sans-serif;
  margin-bottom: 15px;
}

.content p {
  line-height: 1.6;
  margin-bottom: 10px;
}

Vậy là ta đã hoàn thành trang "navigation"!

Làm xong được thành quả như trang này:

TRANG NAVIGATION