mirror of
https://github.com/netbirdio/docs.git
synced 2026-04-15 23:16:36 +00:00
add TOC
This commit is contained in:
@@ -56,7 +56,7 @@ function rehypeSlugify() {
|
||||
return (tree) => {
|
||||
let slugify = slugifyWithCounter()
|
||||
visit(tree, 'element', (node) => {
|
||||
if (node.tagName === 'h2' && !node.properties.id) {
|
||||
if ( ['h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) {
|
||||
node.properties.id = slugify(toString(node))
|
||||
}
|
||||
})
|
||||
@@ -97,10 +97,11 @@ function getSections(node) {
|
||||
let sections = []
|
||||
|
||||
for (let child of node.children ?? []) {
|
||||
if (child.type === 'element' && child.tagName === 'h2') {
|
||||
if (child.type === 'element' && ['h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) {
|
||||
sections.push(`{
|
||||
title: ${JSON.stringify(toString(child))},
|
||||
id: ${JSON.stringify(child.properties.id)},
|
||||
tagName: ${JSON.stringify(child.tagName)},
|
||||
...${child.properties.annotation}
|
||||
}`)
|
||||
} else if (child.children) {
|
||||
|
||||
@@ -2,10 +2,12 @@ import nextMDX from '@next/mdx'
|
||||
import { remarkPlugins } from './mdx/remark.mjs'
|
||||
import { rehypePlugins } from './mdx/rehype.mjs'
|
||||
import { recmaPlugins } from './mdx/recma.mjs'
|
||||
import rehypeSlug from "rehype-slug";
|
||||
|
||||
const withMDX = nextMDX({
|
||||
options: {
|
||||
remarkPlugins,
|
||||
// rehypeSlug,
|
||||
rehypePlugins,
|
||||
recmaPlugins,
|
||||
providerImportSource: '@mdx-js/react',
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"recma-nextjs-static-props": "^1.0.0",
|
||||
"rehype-mdx-title": "^2.0.0",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"shiki": "^0.14.2",
|
||||
"tailwindcss": "^3.3.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
import { Button } from '@/components/Button'
|
||||
import {apiNavigation, docsNavigation, navigation} from '@/components/Navigation'
|
||||
import {apiNavigation, docsNavigation, navigation} from '@/components/NavigationAPI'
|
||||
|
||||
function CheckIcon(props) {
|
||||
return (
|
||||
|
||||
@@ -67,18 +67,18 @@ export function Heading({
|
||||
}) {
|
||||
let Component = `h${level}`
|
||||
let ref = useRef()
|
||||
let registerHeading = useSectionStore((s) => s.registerHeading)
|
||||
// let registerHeading = useSectionStore((s) => s.registerHeading)
|
||||
|
||||
let inView = useInView(ref, {
|
||||
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
|
||||
amount: 'all',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (level === 2) {
|
||||
registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 })
|
||||
}
|
||||
})
|
||||
// useEffect(() => {
|
||||
// if (level === 2) {
|
||||
// registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 })
|
||||
// }
|
||||
// })
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,11 +4,11 @@ import { motion } from 'framer-motion'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { Header } from '@/components/Header'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import { NavigationAPI } from '@/components/NavigationAPI'
|
||||
import { Prose } from '@/components/Prose'
|
||||
import { SectionProvider } from '@/components/SectionProvider'
|
||||
|
||||
export function Layout({ children, sections = [] }) {
|
||||
export function LayoutAPI({ children, sections = [] }) {
|
||||
return (
|
||||
<SectionProvider sections={sections}>
|
||||
<div className="lg:ml-72 xl:ml-80">
|
||||
@@ -23,7 +23,7 @@ export function Layout({ children, sections = [] }) {
|
||||
</Link>
|
||||
</div>
|
||||
<Header />
|
||||
<Navigation className="hidden lg:mt-10 lg:block" />
|
||||
<NavigationAPI className="hidden lg:mt-10 lg:block" />
|
||||
</div>
|
||||
</motion.header>
|
||||
<div className="relative px-4 pt-14 sm:px-6 lg:px-8">
|
||||
273
src/components/LayoutDocs.jsx
Normal file
273
src/components/LayoutDocs.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Logo, Logomark } from '@/components/Logo'
|
||||
import { MobileNavigation } from '@/components/MobileNavigation'
|
||||
import { Prose } from '@/components/Prose'
|
||||
import { Search } from '@/components/Search'
|
||||
import {HeroPattern} from "@/components/HeroPattern";
|
||||
import {NavigationDocs} from "@/components/NavigationDocs";
|
||||
import {Header} from "@/components/Header";
|
||||
import {NavigationAPI} from "@/components/NavigationAPI";
|
||||
import {motion} from "framer-motion";
|
||||
import {Footer} from "@/components/Footer";
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: 'Introduction',
|
||||
links: [
|
||||
{ title: 'Getting started', href: '/' },
|
||||
{ title: 'Installation', href: '/docs/installation' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Core concepts',
|
||||
links: [
|
||||
{ title: 'Understanding caching', href: '/docs/understanding-caching' },
|
||||
{
|
||||
title: 'Predicting user behavior',
|
||||
href: '/docs/predicting-user-behavior',
|
||||
},
|
||||
{ title: 'Basics of time-travel', href: '/docs/basics-of-time-travel' },
|
||||
{
|
||||
title: 'Introduction to string theory',
|
||||
href: '/docs/introduction-to-string-theory',
|
||||
},
|
||||
{ title: 'The butterfly effect', href: '/docs/the-butterfly-effect' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Advanced guides',
|
||||
links: [
|
||||
{ title: 'Writing plugins', href: '/docs/writing-plugins' },
|
||||
{ title: 'Neuralink integration', href: '/docs/neuralink-integration' },
|
||||
{ title: 'Temporal paradoxes', href: '/docs/temporal-paradoxes' },
|
||||
{ title: 'Testing', href: '/docs/testing' },
|
||||
{ title: 'Compile-time caching', href: '/docs/compile-time-caching' },
|
||||
{
|
||||
title: 'Predictive data generation',
|
||||
href: '/docs/predictive-data-generation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'API reference',
|
||||
links: [
|
||||
{ title: 'CacheAdvance.predict()', href: '/docs/cacheadvance-predict' },
|
||||
{ title: 'CacheAdvance.flush()', href: '/docs/cacheadvance-flush' },
|
||||
{ title: 'CacheAdvance.revert()', href: '/docs/cacheadvance-revert' },
|
||||
{ title: 'CacheAdvance.regret()', href: '/docs/cacheadvance-regret' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Contributing',
|
||||
links: [
|
||||
{ title: 'How to contribute', href: '/docs/how-to-contribute' },
|
||||
{ title: 'Architecture guide', href: '/docs/architecture-guide' },
|
||||
{ title: 'Design principles', href: '/docs/design-principles' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function GitHubIcon(props) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// function Header({ navigation }) {
|
||||
// let [isScrolled, setIsScrolled] = useState(false)
|
||||
//
|
||||
// useEffect(() => {
|
||||
// function onScroll() {
|
||||
// setIsScrolled(window.scrollY > 0)
|
||||
// }
|
||||
// onScroll()
|
||||
// window.addEventListener('scroll', onScroll, { passive: true })
|
||||
// return () => {
|
||||
// window.removeEventListener('scroll', onScroll)
|
||||
// }
|
||||
// }, [])
|
||||
//
|
||||
// return (
|
||||
// <header
|
||||
// className={clsx(
|
||||
// 'sticky top-0 z-50 flex flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
|
||||
// isScrolled
|
||||
// ? 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
|
||||
// : 'dark:bg-transparent'
|
||||
// )}
|
||||
// >
|
||||
// <div className="mr-6 flex lg:hidden">
|
||||
// <MobileNavigation navigation={navigation} />
|
||||
// </div>
|
||||
// <div className="relative flex flex-grow basis-0 items-center">
|
||||
// <Link href="/" aria-label="Home page">
|
||||
// <Logomark className="h-9 w-9 lg:hidden" />
|
||||
// <Logo className="hidden h-9 w-auto fill-slate-700 dark:fill-sky-100 lg:block" />
|
||||
// </Link>
|
||||
// </div>
|
||||
// <div className="-my-5 mr-6 sm:mr-8 md:mr-0">
|
||||
// <Search />
|
||||
// </div>
|
||||
// <div className="relative flex basis-0 justify-end gap-6 sm:gap-8 md:flex-grow">
|
||||
// <Link href="https://github.com" className="group" aria-label="GitHub">
|
||||
// <GitHubIcon className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
|
||||
// </Link>
|
||||
// </div>
|
||||
// </header>
|
||||
// )
|
||||
// }
|
||||
|
||||
function useTableOfContents(tableOfContents) {
|
||||
let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id)
|
||||
|
||||
let getHeadings = useCallback((tableOfContents) => {
|
||||
return tableOfContents
|
||||
.flatMap((node) => [node.id, ...node.children.map((child) => child.id)])
|
||||
.map((id) => {
|
||||
let el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
let style = window.getComputedStyle(el)
|
||||
let scrollMt = parseFloat(style.scrollMarginTop)
|
||||
|
||||
let top = window.scrollY + el.getBoundingClientRect().top - scrollMt
|
||||
return { id, top }
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (tableOfContents.length === 0) return
|
||||
let headings = getHeadings(tableOfContents)
|
||||
function onScroll() {
|
||||
let top = window.scrollY
|
||||
let current = headings[0].id
|
||||
for (let heading of headings) {
|
||||
if (top >= heading.top) {
|
||||
current = heading.id
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
setCurrentSection(current)
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
}
|
||||
}, [getHeadings, tableOfContents])
|
||||
|
||||
return currentSection
|
||||
}
|
||||
|
||||
export function LayoutDocs({ children, title, tableOfContents }) {
|
||||
let router = useRouter()
|
||||
let isHomePage = router.pathname === '/'
|
||||
let allLinks = navigation.flatMap((section) => section.links)
|
||||
let linkIndex = allLinks.findIndex((link) => link.href === router.pathname)
|
||||
let previousPage = allLinks[linkIndex - 1]
|
||||
let nextPage = allLinks[linkIndex + 1]
|
||||
let section = navigation.find((section) =>
|
||||
section.links.find((link) => link.href === router.pathname)
|
||||
)
|
||||
let currentSection = useTableOfContents(tableOfContents)
|
||||
|
||||
function isActive(section) {
|
||||
if (section.id === currentSection) {
|
||||
return true
|
||||
}
|
||||
if (!section.children) {
|
||||
return false
|
||||
}
|
||||
return section.children.findIndex(isActive) > -1
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*<Header navigation={navigation} />*/}
|
||||
|
||||
<HeroPattern/>
|
||||
<div className="relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12 lg:ml-72 xl:ml-80">
|
||||
<motion.header
|
||||
layoutScroll
|
||||
className="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex"
|
||||
>
|
||||
<div className="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-900/10 lg:px-6 lg:pb-8 lg:pt-4 lg:dark:border-white/10 xl:w-80">
|
||||
<div className="hidden lg:flex">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Logo className="h-6" />
|
||||
</Link>
|
||||
</div>
|
||||
<Header />
|
||||
<NavigationDocs className="hidden lg:mt-10 lg:block" />
|
||||
</div>
|
||||
</motion.header>
|
||||
<div className="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
|
||||
<main className="py-16">
|
||||
<Prose as="article">{children}</Prose>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="hidden xl:sticky xl:top-[4.5rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
||||
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||
{tableOfContents.length > 0 && (
|
||||
<>
|
||||
{/*<h2*/}
|
||||
{/* id="on-this-page-title"*/}
|
||||
{/* className="font-display text-sm font-medium text-slate-900 dark:text-white"*/}
|
||||
{/*>*/}
|
||||
{/* On this page*/}
|
||||
{/*</h2>*/}
|
||||
<ol role="list" className="mt-4 space-y-3 text-sm">
|
||||
{tableOfContents.map((section) => (
|
||||
<li key={section.id}>
|
||||
<h3>
|
||||
<Link
|
||||
href={`#${section.id}`}
|
||||
className={clsx(
|
||||
isActive(section)
|
||||
? 'text-orange-500'
|
||||
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
</Link>
|
||||
</h3>
|
||||
{section.children.length > 0 && (
|
||||
<ol
|
||||
role="list"
|
||||
className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
{section.children.map((subSection) => (
|
||||
<li key={subSection.id}>
|
||||
<Link
|
||||
href={`#${subSection.id}`}
|
||||
className={
|
||||
isActive(subSection)
|
||||
? 'text-orange-500'
|
||||
: 'hover:text-slate-600 dark:hover:text-slate-300'
|
||||
}
|
||||
>
|
||||
{subSection.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
|
||||
import { create } from 'zustand'
|
||||
|
||||
import { Header } from '@/components/Header'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import { NavigationAPI } from '@/components/NavigationAPI'
|
||||
|
||||
function MenuIcon(props) {
|
||||
return (
|
||||
@@ -103,7 +103,7 @@ export function MobileNavigation() {
|
||||
layoutScroll
|
||||
className="fixed bottom-0 left-0 top-14 w-full overflow-y-auto bg-white px-4 pb-4 pt-6 shadow-lg shadow-zinc-900/10 ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 min-[416px]:max-w-sm sm:px-6 sm:pb-10"
|
||||
>
|
||||
<Navigation />
|
||||
<NavigationAPI />
|
||||
</motion.div>
|
||||
</Transition.Child>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -15,7 +15,7 @@ function useInitialValue(value, condition = true) {
|
||||
return condition ? initialValue : value
|
||||
}
|
||||
|
||||
function TopLevelNavItem({ href, children }) {
|
||||
export function TopLevelNavItem({ href, children }) {
|
||||
return (
|
||||
<li className="md:hidden">
|
||||
<Link
|
||||
@@ -28,7 +28,7 @@ function TopLevelNavItem({ href, children }) {
|
||||
)
|
||||
}
|
||||
|
||||
function NavLink({ href, tag, active, isAnchorLink = false, children }) {
|
||||
export function NavLink({ href, tag, active, isAnchorLink = false, children }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
@@ -87,7 +87,7 @@ function VisibleSectionHighlight({ group, pathname }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ActivePageMarker({ group, pathname }) {
|
||||
export function ActivePageMarker({ group, pathname }) {
|
||||
let itemHeight = remToPx(2)
|
||||
let offset = remToPx(0.25)
|
||||
let activePageIndex = group.links.findIndex((link) => link.href === pathname)
|
||||
@@ -248,7 +248,7 @@ export const apiNavigation = [
|
||||
},
|
||||
]
|
||||
|
||||
export function Navigation(props) {
|
||||
export function NavigationAPI(props) {
|
||||
let router = useRouter()
|
||||
return (
|
||||
<nav {...props}>
|
||||
125
src/components/NavigationDocs.jsx
Normal file
125
src/components/NavigationDocs.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
import {ActivePageMarker, docsNavigation, NavLink, TopLevelNavItem} from "@/components/NavigationAPI";
|
||||
import {useIsInsideMobileNavigation} from "@/components/MobileNavigation";
|
||||
import {useSectionStore} from "@/components/SectionProvider";
|
||||
import {AnimatePresence, motion} from "framer-motion";
|
||||
|
||||
export function NavigationDocs(props) {
|
||||
return (
|
||||
<nav {...props}>
|
||||
<ul role="list">
|
||||
<TopLevelNavItem href="https://netbird.io/">Home</TopLevelNavItem>
|
||||
<TopLevelNavItem href="/docs/introductions">Docs</TopLevelNavItem>
|
||||
<TopLevelNavItem href="/ipa/introductions">API</TopLevelNavItem>
|
||||
<TopLevelNavItem href="https://netbird.io/blog/">Blog</TopLevelNavItem>
|
||||
<TopLevelNavItem href="https://github.com/netbirdio/netbird">Github</TopLevelNavItem>
|
||||
<TopLevelNavItem href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Support</TopLevelNavItem>
|
||||
{docsNavigation.map((group, groupIndex) => (
|
||||
<NavigationGroup
|
||||
key={group.title}
|
||||
group={group}
|
||||
className={groupIndex === 0 && 'md:mt-0'}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
// <li key={section.title} className={clsx('relative mt-6', groupIndex === 0 && 'md:mt-0')}>
|
||||
// <h2 className="font-display font-medium text-slate-900 dark:text-white">
|
||||
// {section.title}
|
||||
// </h2>
|
||||
// <ul
|
||||
// role="list"
|
||||
// className="mt-2 space-y-2 border-l-2 border-slate-100 dark:border-slate-800 lg:mt-4 lg:space-y-4 lg:border-slate-200"
|
||||
// >
|
||||
// {section.links.map((link) => (
|
||||
// <li key={link.href} className="relative">
|
||||
// <Link
|
||||
// href={link.href}
|
||||
// className={clsx(
|
||||
// 'block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full',
|
||||
// link.href === router.pathname
|
||||
// ? 'font-semibold text-sky-500 before:bg-sky-500'
|
||||
// : 'text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300'
|
||||
// )}
|
||||
// >
|
||||
// {link.title}
|
||||
// </Link>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </li>
|
||||
|
||||
function NavigationGroup({ group, className }) {
|
||||
// If this is the mobile navigation then we always render the initial
|
||||
// state, so that the state does not change during the close animation.
|
||||
// The state will still update when we re-open (re-render) the navigation.
|
||||
let router = useRouter()
|
||||
|
||||
let isActiveGroup =
|
||||
group.links.findIndex((link) => link.href === router.pathname) !== -1
|
||||
|
||||
|
||||
return (
|
||||
<li className={clsx('relative mt-6', className)}>
|
||||
<motion.h2
|
||||
layout="position"
|
||||
className="text-xs font-semibold text-zinc-900 dark:text-white"
|
||||
>
|
||||
{group.title}
|
||||
</motion.h2>
|
||||
<div className="relative mt-3 pl-2">
|
||||
<motion.div
|
||||
layout
|
||||
className="absolute inset-y-0 left-2 w-px bg-zinc-900/10 dark:bg-white/5"
|
||||
/>
|
||||
<AnimatePresence initial={false}>
|
||||
{isActiveGroup && (
|
||||
<ActivePageMarker group={group} pathname={router.pathname} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<ul role="list" className="border-l border-transparent">
|
||||
{group.links.map((link) => (
|
||||
<motion.li key={link.href} layout="position" className="relative">
|
||||
<NavLink href={link.href} active={link.href === router.pathname}>
|
||||
{link.title}
|
||||
</NavLink>
|
||||
{/*<AnimatePresence mode="popLayout" initial={false}>*/}
|
||||
{/* {link.href === router.pathname && sections.length > 0 && (*/}
|
||||
{/* <motion.ul*/}
|
||||
{/* role="list"*/}
|
||||
{/* initial={{ opacity: 0 }}*/}
|
||||
{/* animate={{*/}
|
||||
{/* opacity: 1,*/}
|
||||
{/* transition: { delay: 0.1 },*/}
|
||||
{/* }}*/}
|
||||
{/* exit={{*/}
|
||||
{/* opacity: 0,*/}
|
||||
{/* transition: { duration: 0.15 },*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* {sections.map((section) => (*/}
|
||||
{/* <li key={section.id}>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* href={`${link.href}#${section.id}`}*/}
|
||||
{/* tag={section.tag}*/}
|
||||
{/* isAnchorLink*/}
|
||||
{/* >*/}
|
||||
{/* {section.title}*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* ))}*/}
|
||||
{/* </motion.ul>*/}
|
||||
{/* )}*/}
|
||||
{/*</AnimatePresence>*/}
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,18 @@ export const h2 = function H2(props) {
|
||||
return <Heading level={2} {...props} />
|
||||
}
|
||||
|
||||
export const h3 = function H3(props) {
|
||||
return <Heading level={3} {...props} />
|
||||
}
|
||||
|
||||
export const h4 = function H4(props) {
|
||||
return <Heading level={4} {...props} />
|
||||
}
|
||||
|
||||
export const h5 = function H5(props) {
|
||||
return <Heading level={5} {...props} />
|
||||
}
|
||||
|
||||
function InfoIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||
|
||||
@@ -2,12 +2,14 @@ import Head from 'next/head'
|
||||
import { Router, useRouter } from 'next/router'
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
|
||||
import { Layout } from '@/components/Layout'
|
||||
import { LayoutAPI } from '@/components/LayoutAPI'
|
||||
import * as mdxComponents from '@/components/mdx'
|
||||
import { useMobileNavigationStore } from '@/components/MobileNavigation'
|
||||
|
||||
import '@/styles/tailwind.css'
|
||||
import 'focus-visible'
|
||||
import {LayoutDocs} from "@/components/LayoutDocs";
|
||||
import {slugifyWithCounter} from "@sindresorhus/slugify";
|
||||
|
||||
function onRouteChange() {
|
||||
useMobileNavigationStore.getState().close()
|
||||
@@ -18,7 +20,7 @@ Router.events.on('hashChangeStart', onRouteChange)
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
let router = useRouter()
|
||||
|
||||
let tableOfContents = collectHeadings(pageProps.sections)
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -31,10 +33,45 @@ export default function App({ Component, pageProps }) {
|
||||
<meta name="description" content={pageProps.description} />
|
||||
</Head>
|
||||
<MDXProvider components={mdxComponents}>
|
||||
<Layout {...pageProps}>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
|
||||
{router.pathname.startsWith("/docs") ? (
|
||||
<LayoutDocs title={pageProps.title.toString()} tableOfContents={tableOfContents} {...pageProps}>
|
||||
<Component {...pageProps} />
|
||||
</LayoutDocs>
|
||||
) : (
|
||||
<LayoutAPI {...pageProps}>
|
||||
<Component {...pageProps} />
|
||||
</LayoutAPI>
|
||||
)}
|
||||
</MDXProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function collectHeadings(sections, slugify = slugifyWithCounter()) {
|
||||
let output = []
|
||||
|
||||
for (let section of sections) {
|
||||
if (section.tagName === 'h2' || section.tagName === 'h3') {
|
||||
let title = section.title
|
||||
let id = section.id
|
||||
if (section.tagName === 'h3') {
|
||||
if (!output[output.length - 1]) {
|
||||
throw new Error(
|
||||
'Cannot add `h3` to table of contents without a preceding `h2`'
|
||||
)
|
||||
}
|
||||
output[output.length - 1].children.push({
|
||||
id,
|
||||
title,
|
||||
})
|
||||
} else {
|
||||
output.push({ id, title, children: [] })
|
||||
}
|
||||
}
|
||||
|
||||
output.push(...collectHeadings(output.children ?? [], slugify))
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {HeroPattern} from "@/components/HeroPattern";
|
||||
|
||||
# NetBird vs. Traditional VPN
|
||||
|
||||
### Traditional VPN challenges
|
||||
## Traditional VPN challenges
|
||||
In the traditional VPN model, everything converges on a centralized, protected network where all the clients are connecting to a central VPN server.
|
||||
|
||||
An increasing amount of connections can easily overload the VPN server.
|
||||
@@ -18,7 +18,7 @@ Configuring firewalls, setting up NATs, SSO integration, and managing access con
|
||||
Traditional centralized VPNs are often compared to a [castle-and-moat](https://en.wikipedia.org/wiki/Moat) model
|
||||
in which once accessed, user is trusted and can access critical infrastructure and resources without any restrictions.
|
||||
|
||||
### NetBird benefits
|
||||
## NetBird benefits
|
||||
|
||||
NetBird decentralizes networks using direct point-to-point connections, as opposed to traditional models.
|
||||
Consequently, network performance is increased since traffic flows directly between the machines bypassing VPN servers or gateways.
|
||||
|
||||
Reference in New Issue
Block a user