> Agent-readable docs index: /llms.txt. Download /docs.zip to grep all markdown files locally.

---
title: Custom Entry
description: Mount Holocron docs alongside your own API routes, pages, and middleware in a single Spiceflow app.
icon: puzzle
---

# Custom Entry

Holocron can be mounted as a **child app** inside your existing Spiceflow project. This lets you ship docs, API routes, auth, webhooks, and custom pages all from a single server.

## When to use this

* You already have a Spiceflow app and want to add a `/docs` section
* You need middleware (auth, logging, headers) to wrap both your routes and the docs
* You want API routes like `/api/chat` living next to your documentation

## Keep docs in a subfolder

When mounting Holocron alongside your own app, **always put MDX files inside a subfolder** like `docs/`. This way all documentation lives under `/docs/*` and won't collide with your app routes like `/api`, `/login`, `/dashboard`, or `/pricing`.

Without a subfolder, a page like `configuration.mdx` maps to `/configuration`, which could easily conflict with a current or future app route. With a `docs/` prefix it becomes `/docs/configuration` and stays cleanly isolated. As your app grows you never have to worry about a new feature route clashing with a docs page.

```diagram
my-project/
├── docs/               ← all MDX pages live here
│   ├── getting-started.mdx
│   └── configuration.mdx
├── docs.json
├── server.tsx
├── vite.config.ts
└── package.json
```

Reference pages in `docs.json` with the `docs/` prefix:

```json
{
  "navigation": [
    {
      "group": "Guides",
      "pages": ["docs/getting-started", "docs/configuration"]
    }
  ]
}
```

Page slugs map directly to file paths. `docs/getting-started` resolves to `docs/getting-started.mdx` and is served at `/docs/getting-started`.

## Setup

Pass `entry` to the holocron plugin pointing to your Spiceflow server file:

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import { holocron } from '@holocron.so/vite'

export default defineConfig({
  plugins: [
    holocron({ entry: './src/server.tsx' }),
  ],
})
```

Then in your server file, import the holocron app and mount it with `.use()`:

```tsx
// src/server.tsx
import { Spiceflow } from 'spiceflow'
import { app as holocronApp } from '@holocron.so/vite/app'

export const app = new Spiceflow()
  // your middleware runs on every request, including docs pages
  .use(async ({ request }, next) => {
    const res = await next()
    if (res) res.headers.set('x-custom-header', 'yes')
    return res
  })
  // your own API routes
  .get('/api/hello', () => ({ hello: 'world' }))
  .get('/api/echo/:name', ({ params }) => ({ name: params.name }))
  // your own pages with your own layouts
  .layout('/dashboard', ({ children }) => (
    <html lang='en'>
      <head><title>Dashboard</title></head>
      <body>{children}</body>
    </html>
  ))
  .page('/dashboard', () => <h1>My Dashboard</h1>)
  // mount holocron last — it handles all docs pages
  .use(holocronApp)

void app.listen(3000)
```

Holocron registers routes for every page in your `docs.json` navigation. Your own routes take priority because they're registered first.

## Middleware

Middleware registered with `.use()` before `.use(holocronApp)` runs on **every request**, including doc pages. This is useful for auth checks, analytics headers, or request logging.

```tsx
export const app = new Spiceflow()
  .use(async ({ request }, next) => {
    console.log(request.method, request.url)
    return next()
  })
  .use(holocronApp)
```

## Custom CSS

Holocron includes Tailwind CSS automatically. **Do not add `@import 'tailwindcss'` in your own CSS files.** A second import creates duplicate style layers that break layout, spacing, and cascade order.

## Tailwind references

Use **`@reference`** instead of `@import`. It gives your CSS access to Tailwind's theme variables, `@apply`, and `@variant` without emitting duplicate styles:

```css
/* src/globals.css */
@reference "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

:root {
  --primary: #e11d48;
  --background: #fafafa;

  @variant dark {
    --background: #0a0a0a;
    --primary: #fb7185;
  }
}
```

## Dark mode variant limitation

`@custom-variant dark` must be repeated in every Tailwind-processed CSS file that uses `@variant dark`. This is a Tailwind limitation: `@custom-variant` is a **compile-time directive**, not a runtime CSS variable or inherited browser setting.

Holocron defines the dark variant in its own stylesheet, but that only affects CSS compiled in Holocron's Tailwind processing context. Your custom entry CSS is processed as a separate stylesheet, so Tailwind does not automatically know that Holocron uses class-based dark mode.

## Required dark variant

If you omit this line:

```css
@custom-variant dark (&:where(.dark, .dark *));
```

then `@variant dark` in your CSS can compile to Tailwind's default dark behavior, which may follow the browser system media query. That makes your app theme disagree with Holocron's persisted toggle, because Holocron stores the selected theme by adding or removing `.dark` on `<html>`.

## Shared theme cookie

Holocron persists the selected mode in the **`color-theme`** cookie and mirrors it on `<html class="dark">`. If your app has its own theme toggle outside the docs layout, use the same cookie so both your pages and Holocron pages share one persisted state:

```tsx
'use client'

function setTheme(theme: 'light' | 'dark') {
  document.documentElement.classList.toggle('dark', theme === 'dark')
  document.cookie = `color-theme=${theme}; Path=/; Max-Age=31536000; SameSite=Lax`
}

export function ThemeToggle() {
  function toggle() {
    const isDark = document.documentElement.classList.contains('dark')
    setTheme(isDark ? 'light' : 'dark')
  }

  return <button onClick={toggle}>Toggle theme</button>
}
```

## Server-rendered theme class

If your custom pages render their own `<html>` shell, read the **same cookie** on the server and set the initial class before the page paints:

```tsx
function getInitialThemeClass(request: Request) {
  const cookie = request.headers.get('cookie') ?? ''
  return /(?:^|;\s*)color-theme=dark(?:;|$)/.test(cookie) ? 'dark' : undefined
}

export const app = new Spiceflow()
  .layout('/dashboard/*', ({ children, request }) => (
    <html lang='en' className={getInitialThemeClass(request)} suppressHydrationWarning>
      <body>{children}</body>
    </html>
  ))
```

## Importing custom CSS

If your CSS file is not named **`global.css`** or **`style.css`** at the project root, import it normally in your server entry:

```tsx
import './globals.css'
```

Your custom properties override Holocron's defaults because Holocron puts its default tokens in a low-priority cascade layer. Normal unlayered app CSS wins regardless of stylesheet load order.

## Custom homepage

With docs nested in `docs/`, your `/` route stays free for a custom homepage. Holocron will not redirect `/` to the first doc page when a parent route already handles it.

```tsx
// server.tsx
import { Spiceflow } from 'spiceflow'
import { app as holocronApp } from '@holocron.so/vite/app'

export const app = new Spiceflow()
  .page('/', () => (
    <html lang='en'>
      <head><title>My Product</title></head>
      <body>
        <h1>Welcome to My Product</h1>
        <a href='/docs/getting-started'>Read the docs</a>
      </body>
    </html>
  ))
  .get('/api/hello', () => ({ hello: 'world' }))
  .use(holocronApp)

export default {
  fetch(request: Request) {
    return app.handle(request)
  },
}
```

## Real-world example

The [holocron.so website](https://github.com/remorses/holocron/tree/main/website) uses this pattern. It mounts holocron docs alongside auth routes (better-auth with Google login), an AI gateway proxy, and a device authorization flow.


---

*Powered by [holocron.so](https://holocron.so)*
