Skip to content

Migrate documentation from VuePress 2 to Fumadocs - #144

Open
raspdevpy wants to merge 27 commits into
betafrom
fumadocs-migration
Open

Migrate documentation from VuePress 2 to Fumadocs#144
raspdevpy wants to merge 27 commits into
betafrom
fumadocs-migration

Conversation

@raspdevpy

Copy link
Copy Markdown
Owner

VuePress 2 never shipped a stable release; the site was pinned to 2.0.0-rc.31. This moves the docs to Fumadocs 16 on Next.js 16, exported as a static build so the existing nginx container still serves it.

Every one of the 624 page URLs is preserved. Function pages live under a content/docs/(functions)/ folder group, which nests them under a "Functions" heading in the sidebar without appearing in the URL, so /Member/kick stays /Member/kick.

Content:

  • 624 .md -> .mdx, with an H1 -> frontmatter title pass
  • ::: tip|warning|danger|info -> , ::: details ->
  • Vue attribute bindings, unclosed void tags, bare braces and stray < fixed for MDX's stricter parser
  • @discord-message-components/vue (unmaintained since 2022) replaced with @skyra/discord-components-react; these are web components and render client-side only

Build pipeline:

  • the three VuePress markdown replacers are now remark plugins (cooldown sections, $function auto-linking, cached remote images)
  • sidebar.js weight ordering translated into 38 meta.json files
  • search moves to Fumadocs' static Orama index, still fed the Badge-derived tags the old parseTags.js supplied
  • docs-pages.json is regenerated post-build, keeping its published shape

Also fixed along the way:

  • $modal/$channel/$poll/$button auto-linked to Trigger pages instead of the functions, because the old name->path scan was last-wins
  • three pages with unclosed fences or duplicated container openers that were already rendering incorrectly
  • nginx served everything uncompressed; gzip cuts a page from 294 KB to 28 KB and the search index from 8.6 MB to 1.4 MB
  • "/" 301-redirected to "/index"

Tooling moves from npm to pnpm.

@raspdevpy
raspdevpy requested a review from NervN August 3, 2026 08:08
@raspdevpy

Copy link
Copy Markdown
Owner Author

VuePress 2 → Fumadocs: before / after

All figures measured on the same machine (Apple silicon, 8 build workers), cold caches, main vs fumadocs-migration.
The old site was rebuilt from main in a clean worktree to get real numbers rather than estimates.

Build

before (VuePress 2.0.0-rc.31) after (Fumadocs 16 / Next 16)
Full build 123.7 s 12.5 s 9.9× faster
↳ compile 1.1 s 4.0 s
↳ render pages 118.8 s 2.0 s 59× faster
↳ typecheck 1.3 s new
docs-pages.json (inline) 3.6 s
Pages rendered 625 626
Heap flag needed --max-old-space-size=8192 none

The old build spent 96 % of its time rendering pages one at a time. The new one prerenders 1253 routes
(624 pages + 624 markdown exports + search index) across 8 workers.

Note: the "~10 minutes" figure we'd assumed was wrong — the old build is 2 m 04 s, not 10 minutes.
Still a 10× win, just from a lower starting point.

What a reader actually downloads

Neither the old nor the new nginx config had gzip enabled. This PR turns it on.

before after
/Message/sendMessage over the wire 137 KB (uncompressed) 27 KB (gzipped) 5× less
Home page 111 KB 24 KB
Search index n/a (per-page JSON) 1.4 MB gzipped, fetched once

Raw page size did grow (137 KB → 294 KB) because Fumadocs inlines the navigation tree and the RSC
payload into every page. Gzip more than absorbs it — 91 % compression, because that inlined tree is
highly repetitive.

Disk / image footprint — this got worse

before after
Build output 92 MB 504 MB 5.5× bigger
Docker image 614 MB
node_modules 154 MB (213 top-level) 519 MB (17 top-level)

The output growth is Next's per-route RSC prefetch payloads (4374 __next*.txt files, 201 MB) plus
the larger HTML. It does not affect what users download — only registry storage and pull time.

Open decision: cutting this needs the Fumadocs root folder feature so the sidebar renders only
the active section instead of all 624 entries. That converts the single scrolling sidebar into
switchable tabs — a visible navigation change, so it's deliberately not in this PR.

Tooling

before after
Package manager npm (--legacy-peer-deps required) pnpm
Framework status VuePress 2 release candidate — never shipped stable Fumadocs 16 stable
Discord components @discord-message-components/vue (last published 2022) @skyra/discord-components-react (maintained)
Search @vuepress/plugin-search Fumadocs static Orama index
Dev server vuepress dev next dev
CI none none (unchanged)

Behaviour changes worth knowing

  • Discord message blocks render client-side only. They're Lit web components; there's no
    server-rendering story for them. The blocks are absent from on hydration.
  • ::: tip renders blue, not green, and callouts with no explicit title no longer show a default
    "TIP"/"WARNING" heading.
  • / no longer 301-redirects to /index.
  • One URL changed: /notUsed/perms&privacy → `/notUsed/pee Next's
    static-param round-trip; the page is unlinked and absent from the sidebar.

Fixed in passing

  • $modal, $channel, $poll, $button auto-linked to Trigger pages instead of the functions —
    the old name→path scan was last-wins.
  • Three pages with unclosed code fences or duplicated containey rendering
    incorrectly on the live site (ObjectSet, awaitButton, getReactions).
  • gzip was never enabled on nginx.

@raspdevpy

Copy link
Copy Markdown
Owner Author

Content preservation: verification

Every guide/**/*.md on main was compared against its content/docs/**/*.mdx counterpart on this
branch. The check runs on the source files, not rendered HTML — the old Vue components
server-rendered their contents while the new web components render client-side, so comparing HTML
would report differences that aren't content.

Each side is reduced to a canonical form that strips only the syntax this migration was supposed to
change (containers, JSX/HTML tags, MDX escaping, link targets, emphasis). Anything surviving that is a
real difference and is listed below.

Inventory

result
Pages before 624
Pages after 624
Pages compared 624
Missing after migration 0
Unexpectedly added 0
URL parity 624 / 624 identical (one intentional rename, below)

Two paths changed deliberately: guide/README.mdcontent/docs/index.mdx (the home page), and
notUsed/perms&privacynotUsed/perms-privacy (the & broke Next's static-param round-trip; the
page is unlinked and not in the sidebar).

Automated comparison

Axis Pages differing Verdict
Fenced code blocks (722 blocks, byte-for-byte) 1 1 intentional fix
Badge / tag values 0 identical
Image URLs 0 identical
Headings 6 all explained
Prose text 69 all explained

Code blocks are the strongest signal for this project — every $function example, every snippet.
721 of 722 are byte-identical. The one exception is Text/Object/ObjectSet, where the source had
an unclosed ```php fence that swallowed the rest of the page; closing it moved text out of the
code block and into prose. That page was rendering incorrectly on the live site.

Every flagged difference, classified

Prose — 69 pages

Count Cause Content changed?
56 Function difficultyFunction difficulty: (colon added for consistency) no — punctuation
6 Templates/* — hyphen in the page title normalised differently on each side no — checker artifact
3 Text/indexOf, Text/textSlice, Threads/thread — escaped \< and quoted Accordion titles no — checker artifact
3 Deliberate fixes: rolePerms (page had no H1, title supplied), awaitButton (duplicated ::: details opener removed), ObjectSet (fence close
d) yes — all three were pre-existing bugs
1 Trigger/1.triggers — links moved out of headings (Fumadocs wraps headings in anchors, so a link inside one is invalid HTML) no — same text, s
ame targets

Headings — 6 pages

Count Cause Content changed?
4 $charCount[text] etc. — the checker strips [ ] from headings but not from frontmatter titles no — checker artifact
1 Role/rolePerms — page had no H1; title supplied yes, intentional
1 Server/serverDescription — this page contains itself twice in the source, with the second # serverDescription glued onto the Tags line. **Pre
served verbatim, including the bug.** no

The Function difficulty / Tags lines moved from h5/h6 headings to bold paragraphs across 505
pages. That's intentional — Fumadocs serialises heading contents into a module-level table of
contents where MDX components aren't in scope, so ##### Function difficulty: <Badge/> fails the
build outright. The text is unchanged.

Reproducing this

git worktree add /tmp/old-site main
cd /tmp/old-site && npm ci --legacy-peer-deps
node verify-content.mjs /tmp/old-site/guide ./content/docs

Conclusion

No page was lost, added, or silently rewritten. Every difference the checker found is either a
normalisation artifact of the checker itself, an intentional syntax change required by MDX, or one of
four deliberate corrections to pages that were already broken:

  • Text/Object/ObjectSet — unclosed ```php fence
  • Text/Components/awaitButton — duplicated ::: details Examples opener
  • Message/getReactions — unbalanced container nesting
  • Role/rolePerms — no H1, so no page title

Not addressed here

Three duplicate pages predating this migration were left untouched:

  • Other/syntax is a byte-identical copy of Guide/syntax (hidden from the sidebar)
  • Guide/3.policy is a byte-identical copy of Legal/policy (hidden from the sidebar)
  • $noMentionMessage exists twice with different prose, both visible in the sidebar
    (Message/noMentionMessage and Text/noMentionMessage)

@raspdevpy raspdevpy self-assigned this Aug 3, 2026
raspdevpy and others added 2 commits August 3, 2026 11:43
VuePress 2 never shipped a stable release; the site was pinned to
2.0.0-rc.31. This moves the docs to Fumadocs 16 on Next.js 16, exported
as a static build so the existing nginx container still serves it.

Every one of the 624 page URLs is preserved. Function pages live under a
`content/docs/(functions)/` folder group, which nests them under a
"Functions" heading in the sidebar without appearing in the URL, so
`/Member/kick` stays `/Member/kick`.

Content:
- 624 .md -> .mdx, with an H1 -> frontmatter `title` pass
- `::: tip|warning|danger|info` -> <Callout>, `::: details` -> <Accordion>
- Vue attribute bindings, unclosed void tags, bare braces and stray `<`
  fixed for MDX's stricter parser
- @discord-message-components/vue (unmaintained since 2022) replaced with
  @skyra/discord-components-react; these are web components and render
  client-side only

Build pipeline:
- the three VuePress markdown replacers are now remark plugins
  (cooldown sections, $function auto-linking, cached remote images)
- sidebar.js weight ordering translated into 38 meta.json files
- search moves to Fumadocs' static Orama index, still fed the Badge-derived
  tags the old parseTags.js supplied
- docs-pages.json is regenerated post-build, keeping its published shape

Also fixed along the way:
- $modal/$channel/$poll/$button auto-linked to Trigger pages instead of the
  functions, because the old name->path scan was last-wins
- three pages with unclosed fences or duplicated container openers that
  were already rendering incorrectly
- nginx served everything uncompressed; gzip cuts a page from 294 KB to
  28 KB and the search index from 8.6 MB to 1.4 MB
- "/" 301-redirected to "/index"

Tooling moves from npm to pnpm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
next/image requires intrinsic dimensions. remarkImage supplies them for
local files by rewriting the path into a static import, but external URLs
are only sized by fetching them at build time, which source.config.ts
disables because many linked Discord CDN attachments are dead. Those nodes
keep a plain string src with no width or height.

next/image validates this only outside production, so `next build` rendered
the images while `next dev` returned a 500 on all 70 pages holding one --
and production was shipping <img> tags with no dimensions, causing layout
shift on all 260 remote images. Render those as a plain <img>, which is
what the VuePress site served for them anyway.

Separately, `collapsible: false` is unusable in fumadocs 16.14.0: the docs
layout passes className as a render-prop function, which the shared
SidebarFolderTrigger forwards to a bare <div> when a folder isn't
collapsible. React drops non-string className values, so the Other and
Legal folder headers rendered with no styling at all. defaultOpen alone
keeps them expanded and correctly styled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@raspdevpy
raspdevpy force-pushed the fumadocs-migration branch from 9257ae5 to ea61d44 Compare August 3, 2026 09:44
@raspdevpy
raspdevpy changed the base branch from main to beta August 3, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants