Skip to main content
Glama
Docs.tsx11.2 kB
import { useState, useEffect, useCallback } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeHighlight from "rehype-highlight"; import rehypeRaw from "rehype-raw"; interface DocItem { slug: string; label: string; file: string; } interface TocItem { id: string; text: string; level: number; } const docs: DocItem[] = [ { slug: "architecture", label: "Architecture", file: "ARCHITECTURE.md" }, { slug: "development", label: "Development", file: "DEVELOPMENT.md" }, { slug: "test", label: "Testing", file: "TEST.md" }, ]; function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } function extractToc(markdown: string): TocItem[] { const headingRegex = /^(#{1,3})\s+(.+)$/gm; const toc: TocItem[] = []; let match; while ((match = headingRegex.exec(markdown)) !== null) { const level = match[1].length; const text = match[2].trim(); // Skip the main title (h1) if (level > 1) { toc.push({ id: slugify(text), text, level, }); } } return toc; } function Docs() { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); const [content, setContent] = useState<string>(""); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [toc, setToc] = useState<TocItem[]>([]); const [activeSection, setActiveSection] = useState<string>(""); const [sidebarOpen, setSidebarOpen] = useState(false); // Default to architecture if no slug const currentSlug = slug || "architecture"; const currentDoc = docs.find((d) => d.slug === currentSlug) || docs[0]; // Redirect to /docs/architecture if accessing /docs useEffect(() => { if (!slug) { navigate("/docs/architecture", { replace: true }); } }, [slug, navigate]); // Fetch markdown content useEffect(() => { setLoading(true); setError(null); const baseUrl = import.meta.env.BASE_URL || "/"; const fullUrl = `${baseUrl}content/${currentDoc.file}`; fetch(fullUrl) .then((res) => { if (!res.ok) throw new Error(`Failed to load: ${res.status}`); return res.text(); }) .then((text) => { setContent(text); setToc(extractToc(text)); setLoading(false); }) .catch((err) => { setError(err.message); setLoading(false); }); }, [currentDoc.file]); // Track active section on scroll useEffect(() => { const handleScroll = () => { const headings = document.querySelectorAll(".markdown-body h2, .markdown-body h3"); let currentActive = ""; headings.forEach((heading) => { const rect = heading.getBoundingClientRect(); if (rect.top <= 100) { currentActive = heading.id; } }); setActiveSection(currentActive); }; window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []); // Handle internal markdown links const handleLinkClick = useCallback( (e: React.MouseEvent<HTMLAnchorElement>, href: string) => { // Handle internal .md links if (href.endsWith(".md") || href.includes(".md#")) { e.preventDefault(); const [file, anchor] = href.split("#"); const targetDoc = docs.find( (d) => d.file.toLowerCase() === file.replace("./", "").toLowerCase() ); if (targetDoc) { navigate(`/docs/${targetDoc.slug}${anchor ? `#${anchor}` : ""}`); } } // Handle anchor links else if (href.startsWith("#")) { e.preventDefault(); const element = document.getElementById(href.slice(1)); if (element) { element.scrollIntoView({ behavior: "smooth" }); } } }, [navigate] ); return ( <div className="docs-layout"> {/* Mobile sidebar toggle */} <button onClick={() => setSidebarOpen(!sidebarOpen)} className="docs-sidebar-toggle lg:hidden fixed bottom-4 right-4 z-50 bg-primary-01-500 text-white p-3 rounded-full shadow-lg" aria-label="Toggle sidebar" > <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> </svg> </button> {/* Sidebar */} <aside className={`docs-sidebar ${sidebarOpen ? "open" : ""}`}> <div className="docs-sidebar-content"> {/* Document list */} <nav className="docs-nav"> <h3 className="docs-nav-title">Documentation</h3> <ul className="docs-nav-list"> {docs.map((doc) => ( <li key={doc.slug}> <Link to={`/docs/${doc.slug}`} onClick={() => setSidebarOpen(false)} className={`docs-nav-link ${currentSlug === doc.slug ? "active" : ""}`} > {doc.label} </Link> </li> ))} </ul> </nav> {/* Table of Contents */} {toc.length > 0 && ( <nav className="docs-toc"> <h3 className="docs-toc-title">On This Page</h3> <ul className="docs-toc-list"> {toc.map((item) => ( <li key={item.id} className={`docs-toc-item level-${item.level}`} > <a href={`#${item.id}`} onClick={(e) => { e.preventDefault(); setSidebarOpen(false); const element = document.getElementById(item.id); if (element) { element.scrollIntoView({ behavior: "smooth" }); } }} className={`docs-toc-link ${activeSection === item.id ? "active" : ""}`} > {item.text} </a> </li> ))} </ul> </nav> )} </div> </aside> {/* Backdrop for mobile */} {sidebarOpen && ( <div className="docs-backdrop lg:hidden" onClick={() => setSidebarOpen(false)} /> )} {/* Main content */} <main className="docs-main"> {loading && ( <div className="flex items-center justify-center py-12"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-01-500"></div> </div> )} {error && ( <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"> <p className="text-red-700 dark:text-red-400">Error loading content: {error}</p> </div> )} {!loading && !error && ( <div className="markdown-body"> <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeHighlight]} components={{ // Add IDs to headings for anchor links h1: ({ children }) => { const text = String(children); return <h1 id={slugify(text)}>{children}</h1>; }, h2: ({ children }) => { const text = String(children); return <h2 id={slugify(text)}>{children}</h2>; }, h3: ({ children }) => { const text = String(children); return <h3 id={slugify(text)}>{children}</h3>; }, // Handle links a: ({ href, children }) => { if (!href) return <span>{children}</span>; // Internal markdown or anchor links if (href.endsWith(".md") || href.includes(".md#") || href.startsWith("#")) { return ( <a href={href} onClick={(e) => handleLinkClick(e, href)} className="text-primary-01-500 hover:text-primary-01-600 hover:underline" > {children} </a> ); } // External links return ( <a href={href} target="_blank" rel="noopener noreferrer" className="text-primary-01-500 hover:text-primary-01-600 hover:underline" > {children} </a> ); }, // Handle images with base URL img: ({ src, alt }) => { const baseUrl = import.meta.env.BASE_URL || "/"; let imgSrc = src; if (src?.startsWith("./assets/")) { imgSrc = `${baseUrl}assets/${src.replace("./assets/", "")}`; } else if (src?.startsWith("./")) { imgSrc = `${baseUrl}${src.replace("./", "")}`; } return ( <img src={imgSrc} alt={alt || ""} className="max-w-full h-auto my-4 rounded-lg border border-grey-200 dark:border-primary-02-700" /> ); }, }} > {content} </ReactMarkdown> </div> )} {/* Previous/Next navigation */} {!loading && !error && ( <nav className="docs-pagination"> {docs.findIndex((d) => d.slug === currentSlug) > 0 && ( <Link to={`/docs/${docs[docs.findIndex((d) => d.slug === currentSlug) - 1].slug}`} className="docs-pagination-link prev" > <span className="docs-pagination-label">Previous</span> <span className="docs-pagination-title"> {docs[docs.findIndex((d) => d.slug === currentSlug) - 1].label} </span> </Link> )} {docs.findIndex((d) => d.slug === currentSlug) < docs.length - 1 && ( <Link to={`/docs/${docs[docs.findIndex((d) => d.slug === currentSlug) + 1].slug}`} className="docs-pagination-link next" > <span className="docs-pagination-label">Next</span> <span className="docs-pagination-title"> {docs[docs.findIndex((d) => d.slug === currentSlug) + 1].label} </span> </Link> )} </nav> )} </main> </div> ); } export default Docs;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MerzoukeMansouri/adeo-mozaic-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server