React

Vite + React with the Content instance plugin and content HMR.
Setting up your own app? Follow the Vite integration guide step by step.
App.tsx
import { useEffect, useState, useSyncExternalStore } from 'react'
import { MarkdownDocument } from '@comark/react'
import { content, getContentVersion, subscribeToContent } from './content'
import { ContentFile } from 'comark-content'

type Item = Awaited<ReturnType<typeof content.list>>[number]

function usePathname() {
  const [path, setPath] = useState(window.location.pathname)

  useEffect(() => {
    const syncPath = () => setPath(window.location.pathname)

    window.addEventListener('popstate', syncPath)
    return () => window.removeEventListener('popstate', syncPath)
  }, [])

  return [path, setPath] as const
}

export default function App() {
  const [path, setPath] = usePathname()
  const contentVersion = useSyncExternalStore(subscribeToContent, getContentVersion, getContentVersion)
  const [items, setItems] = useState<Item[]>([])
  const [page, setPage] = useState<ContentFile | null>(null)
  const [pending, setPending] = useState(true)
  const [error, setError] = useState<string | null>(null)

  function navigate(event: React.MouseEvent<HTMLAnchorElement>, to: string) {
    event.preventDefault()
    if (to === path) return

    history.pushState({}, '', to)
    setPath(to)
  }

  useEffect(() => {
    let cancelled = false

    async function load() {
      setPending(true)
      setError(null)

      try {
        const [nextPage, nextItems] = await Promise.all([content.get(path), content.list()])
        if (cancelled) return

        setPage(nextPage ? nextPage : null)
        setItems(nextItems)
        if (!nextPage) setError('Not found')
      } catch (err) {
        if (cancelled) return
        setError((err as Error).message)
        setPage(null)
      } finally {
        if (!cancelled) setPending(false)
      }
    }

    void load()

    return () => {
      cancelled = true
    }
  }, [path, contentVersion])

  return (
    <div className="layout">
      <aside>
        <h2>Pages</h2>
        <nav>
          {items.map((item) => (
            <a
              aria-current={path === item.path ? 'page' : undefined}
              className={path === item.path ? 'active' : undefined}
              href={item.path}
              key={item.path}
              onClick={(event) => navigate(event, item.path)}
            >
              {item.data?.title ?? item.meta.stem}
            </a>
          ))}
        </nav>
      </aside>
      <main>
        {pending ? <p>Loading...</p> : page ? <MarkdownDocument value={page} /> : <p>{error ?? 'Not found'}</p>}
      </main>
    </div>
  )
}