I've moved all my local Obsidian content to the web on
@cloudflare Workers, using the GitHub API,
@tan_stack start, and Fumadocs
Here's a rant about the problems I've faced:
> tried fumadocs-ui with TanStack Start, DocsLayout expects Next.js router context, had to use undocumented fumadocs-ui/provider/tanstack wrapper
> used import.meta.glob for vault .md files, Vite refuses to resolve symlinks, set preserveSymlinks AND server.fs.allow for parent dir
> remark-math parses Full Loss: $$ as inline math start, $$Classification Loss: as closing delimiter, entire document between = one KaTeX block = red error cascade
> wrote preprocessor to split $$ onto own lines, now indented list items break because $$ with 4 spaces = code block not math fence per CommonMark
> closing $$ at 4-space indent doesn't close math block in micromark, requires ≤3 spaces, everything after becomes red KaTeX error
> rehype-katex omits throwOnError from Options type via Omit<>, can't disable red error rendering, only strict: 'ignore' available
> KaTeX chokes on unicode inside accidental math blocks: zero-width spaces (8203), smart quotes (8217), emojis ✅❌ all throw "No character metrics" warnings
> renderMarkdown runs twice (SSR hydration), both execute full remark rehype KaTeX pipeline, client re-parses already-rendered HTML causing duplicate warnings
> switched to pre-rendering KaTeX before remark, used <span> wrapper, remark parses span contents as markdown, closes at first inner </span> not outer
> KaTeX outputs MathML <annotation> with raw LaTeX source, remark leaks annotation text into output showing {DETR} = \lambda{cls} as plain text
> set output: 'html' to strip MathML, switched to <div> wrapper, remark treats as opaque HTML block — now <div> at column 0 breaks nested list context
> deeply indented list items after <div> become code blocks, 4-tab indent (16 spaces) no longer recognized as list continuation after block element
> tried <span style="display:block"> to keep inline context, KaTeX's nested spans still get fragmented by remark's inline HTML parser
> tried custom <math-display> element with rehype plugin to replace post-parse, custom elements aren't in CommonMark type-6 block list but still break lists
> TanStack Start splat routes need params._splat, not documented, found by reading api.trpc.$.tsx scaffold code
> fumadocs-core PageTree.Item url field doesn't auto-encode spaces, manual `encodeURIComponent` needed for "Knowledge Index" → " " paths
> wikilink resolution: Obsidian uses filename lookup not path, built filename→key index, then realized anchors need separate slugify matching heading ID generation
> `[[Page#Section|Alias]]` wikilinks stripped to plain text initially, then added resolver but cross-vault links need vault context not available in renderer
> Shiki highlighting in Workers: had to use JS regex engine not WASM, async codeToHtml inside sync processSync = Promise wrapper hell, moved to loader
> `
#Q question
#A answer` flashcard syntax: regex eats newlines between tokens, had to use `[^\S\n] ` (horizontal whitespace only) to preserve structure
> same-line `
#Q text
#A text` works, multi-line breaks, had to split first then group consecutive `
#Q`/`
#A` lines into `<div class="obsidian-qa">` blocks
> hydration mismatch: fumadocs-ui RootProvider sets `className="dark"` on server, client detects different theme, added `suppressHydrationWarning` to `<html>` and `<body>`
> Grammarly extension injects `data-gr-ext-installed` on body during hydration, triggers mismatch warnings, same `suppressHydrationWarning` fix
> `flattenTree` from fumadocs-core expects `Node[]` not `PageTree.Root`, had to pass `tree.children` not `tree`
> Obsidian image embeds image.png need conversion to standard markdown, built imageByFilename Map with both encoded and decoded keys for lookup
> mermaid code blocks need client-side rendering, marked with .vault-mermaid class, lazy-loaded mermaid.js in useEffect after HTML set
> scroll-to-anchor breaks on SPA navigation, hash exists before element rendered, added retry loop with 5 attempts × 100ms delay
> anchor click handling: href="
#section" needs preventDefault smooth scroll history.pushState, same-page vs cross-page detection via pathname comparison
> Obsidian callouts > !note not standard markdown, need custom remark plugin or regex preprocessing, skipped for now
> nested lists with mixed tabs/spaces: Obsidian uses 4-space equiv tabs, CommonMark interprets 4 spaces as code block in certain contexts
> math inside list items: $$` must be ≤3 spaces from list item content column, Obsidian allows 0-indent which breaks list continuation
> `processSync` can't handle async Shiki highlighting, had to make renderMarkdown async, moved call to route loader for SSR
> KaTeX CSS `?url` import generates hashed asset path, works in dev but needed verification for Worker ASSETS binding in prod
> search API uses `createSearchAPI("advanced")` with structuredData, had to strip markdown syntax for indexable text, regex soup for fenced blocks/links/math
> slugifyAnchor normalization: Obsidian "IOU (Heading)" and "IOU(Heading)" both need same slug, added `\s \(` → `(` replacement before kebab-case
> blank lines inside `$$ math blocks: Obsidian allows, standard CommonMark terminates block, wrote pass to normalize delimiter placement
> \` at end of lines in LaTeX cases environment: \ (backslash space) vs \\ (line break), inconsistent source files cause KaTeX errors
> styling broken via proxy: assets at `/fumadocs/assets/*` correctly proxied but Worker can't find files because path mismatch with actual build output location
> pnpm v10 `ERR_PNPM_IGNORED_BUILDS` hard error: esbuild/sharp/workerd scripts blocked, moved `onlyBuiltDependencies` from `package.json#pnpm` to `pnpm-workspace.yaml`
> fumadocs-ui `.shiki:not(.not-fumadocs-codeblock *)` CSS matches ALL `.shiki` elements, applies padding/position to `.line` spans creating horizontal separator lines between code rows
> Shiki `bundledThemes` not obvious, had to enumerate keys to find available dark themes
> code span colors overridden: fumadocs-ui `code span { color: var(--shiki-light) }` rule wipes all inline token colors from Shiki output
> RootProvider `search` prop API undocumented for TanStack, dug through `.d.ts` files to find `DefaultSearchDialogProps` interface
> DocsLayout `tree` prop needs `PageTree.Root`, had to read bundled `definitions-Cob-Q8-8.d.ts` to understand Item/Folder/Separator structure
> fumadocs-core loader accepts `VirtualFile` objects, could construct manually but easier to build PageTree directly from file paths
> `createMarkdownRenderer` from fumadocs-core uses remark rehype internally, but outputs React component not HTML string, needed different approach
> gray-matter and marked not needed: fumadocs-core has `content/md/frontmatter` and remark pipeline built-in, but as transitive deps not directly importable in pnpm
> `fumadocs-core/mdx-plugins/remark-gfm` re-exports remarkGfm, can use without adding direct dependency
> Cloudflare Worker can't use filesystem at runtime, all content must be bundled at build time via import.meta.glob eager loading
> glob pattern `'../../content/**/*.md'` from src/lib needed symlink in place AND Vite fs.allow config for parent directory
> `$vault.tsx` acts as layout needing `<Outlet />`, `$vault.index.tsx` is vault index, `$vault.$.tsx` is catch-all, file naming convention undocumented
> TanStack Router basepath `/fumadocs` handles routing but Vite base affects asset URLs differently, needed both configured correctly
> mermaid.initialize() must be called before
mermaid.run(), but DOM not ready on hydration, race condition with useEffect timing
> structuredData from search index includes raw markdown, needed regex to strip fenced blocks, wikilinks, math delimiters for clean search text