Fumadocs, dokumentacja wewnątrz aplikacji Next.js
Fumadocs to zestaw bibliotek, które dokładasz do istniejącej aplikacji React, żeby dostać z nich wyszukiwarkę, nawigację, komponenty MDX i strony generowane z definicji OpenAPI. Bieżąca wersja fumadocs-core i fumadocs-ui to 16.15.0, opublikowana w rejestrze npm 21 sierpnia 2026 roku, na licencji MIT.
Biblioteka, a nie osobny generator
Różnica wobec pozostałych narzędzi do dokumentacji nie polega na zestawie funkcji, tylko na tym, kto jest właścicielem aplikacji. Docusaurus to osobny framework: dostajesz jego routing, jego build i jego konwencje. Starlight to motyw dla Astro, więc dokumentacja jest osobnym projektem Astro. Mintlify to platforma hostowana, w której silnik renderujący jest zamknięty. Fumadocs jest czwartym układem: to zależność w Twoim package.json, a strony dokumentacji są zwykłymi trasami Twojej aplikacji.
Praktyczne skutki są konkretne. Nagłówek, stopka i system projektowy produktu są te same, bo to te same komponenty. Wdrożenie jest jedno, bo to jedna aplikacja. Uwierzytelnianie działa na dokumentacji tak samo jak na reszcie serwisu, bo strony przechodzą przez to samo oprogramowanie pośredniczące. Nie ma też kroku synchronizacji między dwoma repozytoriami.
Koszt jest po drugiej stronie. Musisz mieć aplikację React w wersji zgodnej z tym, czego Fumadocs wymaga, a wymagania są ostre. Pakiet fumadocs-ui w wersji 16.15.0 deklaruje zależności równorzędne react w zakresie ^19.2.0, react-dom w tym samym zakresie oraz next w zakresie 16.x.x. Nie ^15 || ^16, tylko sam 16.x.x. Jeśli produkt stoi na Next.js w wersji 15, to warstwa interfejsu tej wersji Fumadocs Ci się nie zainstaluje bez wymuszenia.
Sama nazwa „biblioteka dla Next.js" jest przy tym już nieaktualna. Pakiet fumadocs-core eksportuje adaptery fumadocs-core/framework/next, /framework/react-router, /framework/tanstack, /framework/waku i /framework/astro, a fumadocs-ui ma odpowiadające im dostawców kontekstu pod fumadocs-ui/provider/next i dalej. Zależności równorzędne rdzenia wymieniają react-router w zakresie 7.x.x || 8.x.x i @tanstack/react-router w zakresie 1.x.x, obie oznaczone jako opcjonalne. Next.js jest ścieżką najlepiej obsłużoną, ale nie jedyną.
# nowy projekt od zera
npm create fumadocs-app@latest
# doinstalowanie do istniejącej aplikacji
npm install fumadocs-core fumadocs-ui fumadocs-mdx
# generowanie stron z definicji OpenAPI, osobny pakiet
npm install fumadocs-openapi
# narzędzie do kopiowania komponentów i migracji układów
npx @fumadocs/cli@latestRodzina pakietów, wersje i licencja
Pakiety Fumadocs nie mają wspólnego numeru wersji i to jest pierwsza rzecz, która myli przy aktualizacji. Stan na 22 sierpnia 2026 roku wygląda tak: fumadocs-core 16.15.0 i fumadocs-ui 16.15.0 wydane 21 sierpnia 2026, fumadocs-mdx 15.3.1 tego samego dnia, fumadocs-openapi 11.3.0 również tego dnia, a fumadocs-typescript 5.3.0 i fumadocs-docgen 3.1.0 wcześniej, 7 lipca 2026. Sześć pakietów, sześć niezależnych linii wersji.
Zakresy zależności równorzędnych między nimi są jednak spójne, co sprawdziłem osobno dla każdego pakietu. fumadocs-ui żąda fumadocs-core w wersji dokładnie 16.15.0, bez zakresu. fumadocs-openapi żąda fumadocs-core i fumadocs-ui w zakresie ^16.15.0. fumadocs-mdx i fumadocs-typescript przyjmują ^16.7.0, a fumadocs-docgen ^15.7.2 || ^16.0.0. Wszystkie te warunki spełnia 16.15.0, więc instalacja kompletu przechodzi bez ostrzeżeń. Konsekwencją dokładnego przypięcia w fumadocs-ui jest to, że rdzenia nie da się podnieść osobno: aktualizujesz obie paczki razem albo wcale.
{
"dependencies": {
"fumadocs-core": "16.15.0",
"fumadocs-ui": "16.15.0",
"fumadocs-mdx": "15.3.1",
"fumadocs-openapi": "11.3.0",
"next": "16.1.0",
"react": "19.2.0",
"react-dom": "19.2.0"
}
}Licencję sprawdziłem z trzech stron i wychodzi wzorowo. W repozytorium fuma-nama/fumadocs leży plik LICENSE, bez wariantów pisowni, z tekstem licencji MIT i notą „Copyright (c) 2023 Fuma". Pole license w rejestrze npm ma wartość MIT dla wszystkich sprawdzonych pakietów rodziny. Rozpakowane paczki fumadocs-core, fumadocs-ui i fumadocs-openapi zawierają plik LICENSE identyczny co do bajtu z tym z repozytorium, obok katalogu dist z faktycznym kodem. Adres repozytorium nie przekierowuje nigdzie indziej.
Jeden szczegół nie jest jednorodny i przy audycie zależności trzeba go zobaczyć. Rdzeń ma twardą zależność zbsearch w zakresie ^4.0.0, a ten pakiet jest na licencji Apache 2.0, z plikiem LICENSE.md i notą „Copyright 2023 ZBSearchSearch Inc". Samo Fumadocs jest więc na MIT, ale drzewo zależności już nie w całości. Obie licencje są permisywne, więc dla większości firm to bez znaczenia, ale jeśli prowadzisz listę licencji, wpisz tam obie.
Jest jeszcze pułapka nazewnicza. Polecenie npm install fumadocs nie zainstaluje niczego użytecznego: pakiet fumadocs w rejestrze ma jedną wersję, 0.0.0, opublikowaną 3 października 2024 roku, i jest zajęciem nazwy. Nazwa wydania fumadocs@16.15.0, widoczna w kanale wydań na GitHubie, dotyczy pakietu wewnętrznego z tego monorepozytorium, nie tej paczki w npm. Rzeczywiste narzędzia to @fumadocs/cli w wersji 1.4.1 oraz create-fumadocs-app w wersji 16.1.18.
Rytm wydań głównych i czym płacisz za numer 16
Numer wersji głównej 16 wygląda niepokojąco i sprawdzenie go było najważniejszą częścią przygotowania tego tekstu, bo częsta zmiana wersji głównej to migracje, a migracje to realny koszt utrzymania.
Historia w rejestrze npm jest taka. Wersja 8.0.0 pojawiła się 26 stycznia 2024 roku, 9.0.0 dziewiętnastego lutego 2024, 10.0.0 czwartego marca 2024, 11.0.0 siedemnastego kwietnia 2024, 12.0.0 dziewiątego czerwca 2024, 13.0.0 dwudziestego ósmego lipca 2024, 14.0.0 dwudziestego drugiego października 2024, 15.0.0 dwudziestego dziewiątego stycznia 2025 i 16.0.0 dwudziestego drugiego października 2025. To osiem zmian wersji głównej w 635 dni, czyli średnio jedna co niecałe osiemdziesiąt dni.
Średnia jest tu myląca, bo trend idzie w jedną stronę. Odstęp między 9.0.0 a 10.0.0 wyniósł czternaście dni, między 14.0.0 a 15.0.0 dziewięćdziesiąt dziewięć dni, a między 15.0.0 a 16.0.0 dwieście sześćdziesiąt sześć dni. Linia 16.x jest bieżąca od 304 dni. Projekt wyraźnie się uspokoił w 2025 roku i to jest lepsza prognoza niż średnia z całej historii. Nadal jednak mówimy o narzędziu, które w pierwszym roku życia zmieniało wersję główną co kilka tygodni, a każda taka zmiana ruszała nazwy eksportów i kształt układów.
W rejestrze widnieje też fumadocs-core w wersji 17.0.0 z 1 lutego 2026 roku, ale znacznik latest wskazuje 16.15.0, a sama wersja 17.0.0 jest oznaczona jako wycofana z komunikatem mówiącym wprost, że została opublikowana przypadkiem przez błąd w narzędziu Changesets i nie należy jej używać. Jeśli w Twoim package.json stoi zakres ^16, nic Ci się nie stanie, ale zakres * albo ręczne sięgnięcie po „najwyższy numer" wciągnie Cię w ślepy zaułek.
Trzeci element ryzyka jest organizacyjny. W rejestrze npm fumadocs-core ma dokładnie jednego opiekuna, konto sonmoosans. Kod przyjmuje wkład z zewnątrz, a wydania publikuje robot z ciągłej integracji, ale kontrola nad publikacją i kierunkiem projektu leży w jednym ręku. To układ typowy dla projektu otwartego prowadzonego przez jedną osobę i niesie znane ryzyko: brak umowy wsparcia, którą można wyegzekwować, i pojedynczy punkt awarii.
Ładowanie treści i drzewo stron
Warstwa treści opiera się na funkcji loader, która przyjmuje źródło i zwraca obiekt z gotowym drzewem nawigacji oraz metodami do pobierania stron. Źródłem jest zwykle fumadocs-mdx czytający pliki MDX z dysku, ale może nim być też wirtualne źródło generowane przez fumadocs-openapi.
import { loader } from 'fumadocs-core/source'
import { docs } from '@/.source'
export const source = loader({
source: docs.toFumadocsSource(),
baseUrl: '/docs',
plugins: []
})
// dostępne po utworzeniu
source.pageTree
source.getPage(['getting-started'])
source.getPages()
source.getPageByHref('/docs/getting-started#instalacja')
source.generateParams('slug', 'lang')Opcje LoaderOptions to baseUrl, i18n, url, pageTree, plugins i icon. Wynikowy obiekt daje pageTree, getPageTree(locale), getPage(slugs, language), getPages(language), getPageByHref(href, options) i generateParams(slug, lang). Ta ostatnia metoda wpina się bezpośrednio w generateStaticParams Next.js.
Czego w tym API nie ma, to wersjonowania dokumentacji. Ani fumadocs-core, ani fumadocs-ui nie zawierają w opublikowanym kodzie żadnego mechanizmu wersji: LoaderOptions nie ma takiego pola, a przeszukanie obu paczek pod kątem pojęcia wersjonowania nie daje trafień. Wersjonowanie da się zrobić ręcznie, tworząc osobny loader na każdą wersję i osobny segment trasy, ale to Twoja praca, nie funkcja narzędzia. Docusaurus ma to w rdzeniu i jeśli utrzymujesz dokumentację dla trzech wspieranych wydań produktu, jest to argument rozstrzygający.
Warstwa wizualna to fumadocs-ui z czterema układami dokumentacji: layouts/docs, layouts/notebook, layouts/flux i layouts/glass, plus layouts/home na stronę główną. Każdy układ ma podmienialne gniazda, na przykład layouts/docs/slots/header, slots/sidebar i page/slots/toc, więc pojedynczy fragment można zastąpić własnym komponentem bez przepisywania całości. Stylowanie stoi na Tailwind CSS przez pakiet @fumadocs/tailwind, a prymitywy pochodzą z Radix UI.
Wyszukiwarka i koszt każdej z dróg
Wyszukiwarka jest wbudowana w rdzeń, w odróżnieniu od Docusaurusa, gdzie trzeba ją dołożyć osobno. Silnikiem jest zbsearch w wersji 4, twarda zależność fumadocs-core. To projekt Michele Rivy, następca silnika Orama, i stąd biorą się przestarzałe aliasy w API: oramaStaticClient jest teraz aliasem staticClient, a opcja initOrama aliasem initDB.
Po stronie serwera masz trzy funkcje. createSearchAPI('simple' | 'advanced', options) buduje indeks z listy rekordów, createI18nSearchAPI robi to samo dla wielu języków, a createFromSource(loader, options) bierze indeks prosto z obiektu zwróconego przez loader. Schemat prosty ma pola url, title, breadcrumbs, description, content, keywords i locale. Schemat zaawansowany ma content, page_id, type, breadcrumbs, tags, url, locale oraz embeddings typu vector[512], czyli miejsce na wyszukiwanie wektorowe. Domyślny tokenizator ma wartość multilingual i obsługuje każdy język bez konfiguracji, przez co opcja localeMap jest oznaczona jako przestarzała.
// app/api/search/route.ts
import { createFromSource } from 'fumadocs-core/search/server'
import { source } from '@/lib/source'
export const { GET, staticGET } = createFromSource(source, {
language: 'multilingual',
localeFilter: true,
buildIndex(page) {
return {
id: page.url,
url: page.url,
title: page.data.title,
description: page.data.description,
structuredData: page.data.structuredData,
tag: page.slugs[0]
}
}
})Obiekt SearchAPI ma dwie procedury obsługi: GET(request) odpowiada na zapytania po stronie serwera, a staticGET() zrzuca cały indeks jako jeden plik do pobrania przez przeglądarkę. To jest właśnie rozwidlenie kosztów i trzeba je świadomie wybrać.
Droga pierwsza, GET, nic nie kosztuje w gotówce, ale wymaga działającego procesu serwerowego. Indeks żyje w pamięci tego procesu i przy każdym zimnym starcie funkcji bezserwerowej jest budowany od nowa. Przeglądarka pobiera tylko wyniki. Klient to fetchClient z domyślnym adresem /api/search.
Droga druga, staticGET, działa na eksporcie statycznym i nie potrzebuje serwera. Klientem jest staticClient z opcjami from, domyślnie /api/search, oraz initDB, tag, locale i search. Cena jest po stronie przeglądarki: pobierany jest cały wyeksportowany indeks, w jednym kawałku, zanim pierwsze zapytanie w ogóle zadziała. Sam silnik jest mały. Rozpakowana paczka zbsearch 4.0.0 zawiera w katalogu dist/browser osiem plików ESM o łącznej wadze 20 728 bajtów bez minifikacji, co po kompresji gzip daje 6914 bajtów. Dla porównania: przy Starlighcie zmierzyliśmy, że lokalny indeks Pagefind kosztuje ponad 250 kB stałego narzutu. Zastrzeżenie jest jednak istotne, bo te liczby mierzą różne rzeczy. Te 6,9 kB to sam silnik przed minifikacją przez bundler, a nie indeks. Wagi samego indeksu nie zmierzyłem, bo zależy ona liniowo od objętości dokumentacji, a Pagefind dodatkowo dzieli swój indeks na fragmenty pobierane na żądanie, czego eksport staticGET nie robi. Przy dużej dokumentacji ta różnica architektoniczna działa na niekorzyść Fumadocs.
Droga trzecia to usługa zewnętrzna. Rdzeń dostarcza gotowe presety klienta algoliaClient z polami indexName i client, oramaCloudClient z polami client, index o wartości default albo crawler oraz params, a także warianty dla Mixedbread i dla FlexSearch. Po stronie serwera są funkcje sync do wysyłania dokumentów do Algolii i do Oramy. Kosztów tej drogi nie podam w liczbach, bo nie dało się ich potwierdzić: strona cennika Algolii zwraca 546 kB HTML z nazwami planów, ale bez limitów rekordów i zapytań, które doklejane są dopiero przez JavaScript, a strona cennika Oramy opisuje plan Pro jako stałą opłatę miesięczną plus jednorazowe wdrożenie obejmujące cztery godziny pracy ich zespołu, również bez kwoty w kodzie strony.
Strony generowane z definicji OpenAPI
To jest funkcja, której żadne z trzech porównywanych narzędzi nie ma w rdzeniu, i najmocniejszy powód, żeby sięgnąć po Fumadocs. Odpowiada za nią osobny pakiet fumadocs-openapi, obecnie w wersji 11.3.0.
import { generateFiles } from 'fumadocs-openapi'
import { openapi } from '@/lib/openapi'
await generateFiles({
input: openapi,
output: './content/docs/api',
per: 'operation',
groupBy: 'tag',
index: {
items: [{ path: 'index.mdx', title: 'API Reference' }],
url: { baseUrl: '/docs/api', contentDir: './content/docs' }
},
meta: { folderStyle: 'folder' },
beforeWrite(files) {
// ostatni moment na zmianę zawartości przed zapisem na dysk
}
})Serwer OpenAPI tworzy się osobno i przyjmuje input jako listę ścieżek, adresów URL albo obiekt mapujący identyfikator schematu na plik, a poza tym disableCache i proxyUrl, którego jedynym zadaniem jest omijanie ograniczeń CORS w panelu testowym. Obsługiwane są typy OpenAPIV2, OpenAPIV3, OpenAPIV3_1 i OpenAPIV3_2. Tryb wyjścia ustawia pole per z wartościami operation, czyli strona na każdą operację, tag, file oraz custom. Grupowanie w katalogi ustawia groupBy z wartościami tag, route i none, przy czym wartością domyślną jest none. Generatory przykładów zapytań są dostępne dla siedmiu języków: curl, C#, Go, Java, JavaScript, Python i Rust. Interaktywny panel „wypróbuj" nie jest własnym kodem Fumadocs, tylko integracją z klientem Scalara, deklarowaną jako opcjonalna zależność równorzędna @scalar/api-client-react.
Ograniczenia są dwa i oba dotyczą cyklu życia plików. Po pierwsze, generateFiles zapisuje pliki MDX na dysku, więc wygenerowane strony są artefaktem w repozytorium, który trzeba albo zatwierdzać, albo odtwarzać w procesie budowania. Alternatywą jest staticSource lub dynamicSource podłączane jako loaderPlugin do loader, i wtedy nic nie ląduje na dysku. Po drugie, śledzenie zmian schematu jest ubogie: opcja watch istnieje, ale w typach jest wprost opisana jako przeznaczona do prostych przypadków, ignorująca własne funkcje wejściowe i adresy URL, z zaleceniem, żeby chokidar skonfigurować sobie samemu.
Fumadocs kontra Docusaurus, Starlight i Mintlify
| Cecha | Fumadocs | Docusaurus | Starlight | Mintlify |
|---|---|---|---|---|
| Model | biblioteka w Twojej aplikacji | osobny framework React | motyw dla Astro | platforma hostowana |
| Wymagane środowisko | React 19.2, Next.js 16 lub inny adapter | własny build | Astro | brak, treść w repozytorium |
| Wyszukiwarka w rdzeniu | tak, ZBSearch, serwerowa lub statyczna | nie, dokładana osobno | tak, lokalny Pagefind | tak, po stronie dostawcy |
| Wersjonowanie dokumentacji | brak, robisz sam | tak, w rdzeniu | brak w rdzeniu | po stronie dostawcy |
| Strony z OpenAPI w rdzeniu | tak, fumadocs-openapi | nie | nie | tak |
| Licencja | MIT | MIT | MIT | silnik zamknięty |
| Hosting | Twój, razem z aplikacją | Twój | Twój | dostawcy |
Wybór rozstrzyga się na trzech pytaniach. Czy dokumentacja ma być częścią istniejącego produktu w Reactcie, czy osobnym serwisem. Czy potrzebujesz wersjonowania. Czy chcesz, żeby ktoś inny to hostował. Fumadocs wygrywa, gdy odpowiedzi brzmią: część produktu, nie potrzebuję wersjonowania, hostuję sam. Przegrywa, gdy zespół nie pracuje w Next.js ani w żadnym z obsługiwanych routerów, gdy wersjonowanie jest wymogiem, albo gdy nikt nie chce utrzymywać kodu układu.
Na tym samym fundamencie stoi Nextra, starszy generator dokumentacji dla Next.js, sterowany gotowym motywem zamiast składany z komponentów. Różnica sprowadza się do tego, ile chcesz konfigurować: motyw daje wynik szybciej, biblioteka daje kontrolę. Przed wyborem sprawdź jednak stan wydań, bo tam jest problem: ostatnia wersja pochodzi z grudnia 2025 roku, mimo że commity szły przez cały 2026, więc poprawki leżą w repozytorium poza zasięgiem instalacji z rejestru.
Typowe błędy
Pierwszy to instalacja pakietu fumadocs z npm. To zajęta nazwa w wersji 0.0.0 sprzed dwóch lat. Potrzebujesz fumadocs-core i fumadocs-ui, a do rusztowania projektu create-fumadocs-app.
Drugi to podnoszenie fumadocs-core bez fumadocs-ui. Interfejs przypina rdzeń dokładną wersją, więc menedżer pakietów albo odmówi, albo cicho zainstaluje dwie kopie rdzenia w drzewie zależności.
Trzeci to sięgnięcie po wersję 17.0.0, bo ma wyższy numer. Jest wycofana z adnotacją mówiącą, że powstała przez błąd narzędzia wydawniczego.
Czwarty to założenie, że staticGET jest darmowe. Jest darmowe dla serwera i płatne dla przeglądarki użytkownika, bo cały indeks pobiera się jednym żądaniem przed pierwszym wyszukiwaniem. Przy dokumentacji na kilkaset stron zmierz ten plik, zanim wybierzesz tę drogę.
Piąty to planowanie wersjonowania dokumentacji z założeniem, że narzędzie je ma. Nie ma go w opublikowanym kodzie żadnego z dwóch głównych pakietów.
Szósty to trzymanie wygenerowanych plików OpenAPI w repozytorium bez procesu, który je odświeża. Po zmianie schematu strony rozjadą się cicho, bo nic ich nie unieważnia. Albo generuj je w kroku budowania, albo używaj loaderPlugin zamiast zapisu na dysk.
Siódmy to traktowanie proxyUrl jak funkcji bezpieczeństwa. To obejście ograniczeń CORS dla panelu testowego, nic więcej.
FAQ
Czy Fumadocs wymaga Next.js?
Nie, choć Next.js jest ścieżką najlepiej obsłużoną. Rdzeń eksportuje adaptery dla Next.js, React Router, TanStack Router, Waku i Astro, a zależności równorzędne wymieniają react-router w zakresie 7.x.x || 8.x.x oraz @tanstack/react-router w zakresie 1.x.x. Twardym wymogiem jest React w wersji ^19.2.0, a pakiet fumadocs-ui 16.15.0 dopuszcza wyłącznie next w zakresie 16.x.x.
Czy wyszukiwarka wymaga zewnętrznej usługi?
Nie. Domyślny silnik ZBSearch jest twardą zależnością rdzenia i działa albo w procesie serwerowym przez procedurę GET, albo w całości w przeglądarce przez staticGET i klienta staticClient. Usługi zewnętrzne, czyli Algolia, Orama Cloud i Mixedbread, to opcja dla dużych zbiorów treści, a odpowiadające im pakiety są zadeklarowane jako opcjonalne zależności równorzędne.
Czy Fumadocs obsługuje wersjonowanie dokumentacji?
Nie w rdzeniu. Ani fumadocs-core, ani fumadocs-ui w wersji 16.15.0 nie zawierają mechanizmu wersji, a LoaderOptions nie ma takiego pola. Wersje da się zbudować ręcznie, tworząc osobny loader i osobny segment trasy na każdą wersję, ale utrzymanie tego rozwiązania spada na Ciebie.
Ile kosztuje Fumadocs?
Nic. Wszystkie sprawdzone pakiety rodziny są na licencji MIT, plik LICENSE w repozytorium, pole license w rejestrze npm i zawartość opublikowanych paczek zgadzają się ze sobą, a płatnego wariantu nie ma. Koszt może pojawić się dopiero po stronie zewnętrznej wyszukiwarki, jeśli ją wybierzesz.
Czy wysoki numer wersji głównej oznacza ciągłe migracje?
Kiedyś oznaczał, dziś mniej. Osiem zmian wersji głównej między 8.0.0 z 26 stycznia 2024 a 16.0.0 z 22 października 2025 daje średnio jedną co niecałe osiemdziesiąt dni, ale odstępy rosły z czternastu dni do dwustu sześćdziesięciu sześciu, a linia 16.x jest bieżąca od 304 dni.
Kto utrzymuje projekt?
W rejestrze npm fumadocs-core ma jednego opiekuna, konto sonmoosans. Wydania publikuje robot z ciągłej integracji, a repozytorium przyjmuje wkład z zewnątrz, ale kierunek i prawo publikacji leżą w jednym ręku. Przy wyborze narzędzia na kilka lat to jest czynnik ryzyka, który trzeba wpisać obok zalet, podobnie jak przy każdym projekcie w TypeScripcie prowadzonym przez pojedynczego autora.
Źródła: repozytorium fuma-nama/fumadocs, fumadocs-core w rejestrze npm i dokumentacja projektu.