Mantine NextJS + Fumadocs

Docs

Menu

API

Built-in API routes: search, GitHub releases and version, on a server deployment and in a static export

The template ships three read-only GET routes, all in the app/api folder:

RouteReturns
/api/searchSearch results from the docs, through Orama
/api/github-releasesThe latest releases of the repository set in config/index.ts
/api/versionThe app's version, read from package.json

Each one works in both deploy modes, but answers differently in a static export (see Static export below).

Search API

The full-text search is powered by Orama through fumadocs-core/search/server: the route builds the index from the docs content source itself, with no separate indexing step and no extra build command.

app/api/search/route.ts

const server = createFromSource(source, {
  language: config.search.language,
});

export const GET = process.env.NEXT_PUBLIC_STATIC_EXPORT === '1' ? server.staticGET : server.GET;

On a server deployment

The index stays on the server and the route answers queries: try /api/search?query=mantine. It returns an array of entries, one for each page, heading or paragraph that matches:

[
  {
    "id": "/docs/mantine",
    "type": "page",
    "content": "Mantine",
    "breadcrumbs": ["Documentation", "Examples"],
    "url": "/docs/mantine"
  },
  {
    "id": "/docs/mantine-6",
    "type": "text",
    "content": "Mantine Marquee",
    "url": "/docs/mantine#third-party-components"
  }
]

type is page, heading or text, and url links straight to the page or the section. Without a query, the route returns an empty array.

Client side

The search UI is a Mantine Spotlight fed by the headless useDocsSearch hook: open it with ⌘K (or /). It queries the route on a server deployment, and downloads the index in a static export:

components/docs/DocsSearch.tsx

const { search, setSearch, query } = useDocsSearch(
  isStaticExport ? { type: 'static', from: `${basePath}/api/search` } : { type: 'fetch' }
);

Config

The search language and the filter row above the results (All, Guides, Examples, Reference) live in the config/index.ts file:

config/index.ts

  /**
   * Search configuration (Orama via fumadocs-core)
   * @see /app/api/search/route.ts
   */
  search: {
    language: 'english',
    filters: [
      { label: 'All', prefixes: [] as string[] },
      { label: 'Guides', prefixes: ['/docs/guides'] },
      { label: 'Examples', prefixes: ['/docs/mantine', '/docs/inline-svg'] },
      { label: 'Reference', prefixes: ['/docs/api', '/docs/versioning'] },
    ],
  },

GitHub Release API

On a server deployment, /api/github-releases proxies the GitHub REST API and returns the latest releases of the repository set in config/index.ts: as many as releaseNotes.maxReleases, 10 by default. The Release Notes page is built on it.

Response

{
  "releases": [],
  "status": "ok"
}

releases holds GitHub's own release objects, newest first (tag_name, name, body, published_at, html_url and the rest), exactly as GitHub's List releases endpoint returns them.

Refused requests and errors

CaseStatusBody
No User-Agent header400{ "error": "User agent not found" }
A User-Agent that looks like a crawler (bot, crawl, slurp, spider)403{ "error": "Bots are not allowed" }
GitHub answers with an errorGitHub's own{ "error": "<GitHub's status text>" }
Anything else goes wrong500{ "error": "Internal server error", "details": "…" }

Caching

The call to GitHub is cached for an hour (next: { revalidate: 3600 }), and the route answers with Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400: a CDN such as Vercel's serves the same response for an hour, and a stale one for up to a day while it refreshes.

Config

config/index.ts

  /**
   * GitHub API configuration
   * @see https://docs.github.com/en/rest/reference/repos#releases
   *
   * The GitHub API token is optional for rate limiting.
   * If you want to use it, create a read-only token: a fine-grained token with
   * public repositories (read-only), or a classic token with no scopes.
   *
   * This information is used to fetch the releases from the GitHub API.
   */
  gitHub: {
    repo: 'gfazioli/next-app-fumadocs-template',
    apiUrl: 'https://api.github.com',
    releasesUrl: 'https://api.github.com/repos/gfazioli/next-app-fumadocs-template/releases',
  },

  /**
   * Release notes configuration
   * This is used to link the release notes in the app.
   */
  releaseNotes: {
    url: 'https://github.com/gfazioli/next-app-fumadocs-template/releases',
    maxReleases: 10,
  },

repo is also sent to GitHub as the request's User-Agent, which GitHub requires.

Rate limiting and GITHUB_TOKEN

Without a token, GitHub allows 60 requests per hour from each IP address; with a token, 5,000 per hour. To use one, set the GITHUB_TOKEN environment variable: the route reads it on its own, with no code to change. If GitHub rejects the token (a 401, or a 403 that is not a rate limit), the route retries without it, since public releases can be read anonymously.

A read-only token is enough, as the config comment above says: the repo scope is not needed, and it would grant full access to your private repositories.

Version API

/api/version returns the version field of package.json, for example:

{ "version": "1.0.0" }

Static export

yarn build:static builds a fully static site (output: 'export'), the way this template's own demo is deployed on GitHub Pages. It sets NEXT_PUBLIC_STATIC_EXPORT=1, and NEXT_PUBLIC_BASE_PATH sets the base path (/next-app-fumadocs-template on Pages). Nothing has to be changed by hand: the routes and the components read the same variable. With no server to answer requests, each route is written out once at build time:

  • /api/search becomes the whole Orama index, exported as a static file. The search box downloads it and runs every query in the browser, so the route ignores query there.
  • /api/github-releases becomes a stub, { "error": "Not available in static export", "releases": [] }. The Release Notes page calls the GitHub API straight from the browser instead, within GitHub's 60 requests per hour for each visitor's IP address.
  • /api/version becomes the version of the build.