+ Next.js + Holocron
+
+ This Next.js app mounts a Holocron docs site at{' '}
+
+ /docs
+
+ . The Holocron app is built separately and served via a catch-all route handler.
+
+
+ Go to Docs →
+
+
+ )
+}
diff --git a/example-inside-next/tsconfig.json b/example-inside-next/tsconfig.json
new file mode 100644
index 00000000..3938e07c
--- /dev/null
+++ b/example-inside-next/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/integration-tests/e2e/base-path/base-path.test.ts b/integration-tests/e2e/base-path/base-path.test.ts
index 44c65145..570b343b 100644
--- a/integration-tests/e2e/base-path/base-path.test.ts
+++ b/integration-tests/e2e/base-path/base-path.test.ts
@@ -50,6 +50,38 @@ test.describe("raw markdown under base path", () => {
});
});
+test.describe("API routes under base path", () => {
+ test("POST /docs/holocron-api/chat returns non-404", async ({ request }) => {
+ // The chat endpoint should be reachable under the base path.
+ // It may return 404 with "Assistant is disabled" body (which is the
+ // handler's own response, not a routing 404), or 200 if enabled.
+ const res = await request.post("/docs/holocron-api/chat", {
+ headers: { "content-type": "application/json" },
+ data: JSON.stringify({ message: "hello", modelMessages: [], currentSlug: "/" }),
+ });
+ // The route must exist — a routing-level 404 would mean the base path
+ // prefix is not wired. The handler itself returns 404 when assistant is
+ // disabled, but the body says "Assistant is disabled".
+ const body = await res.text();
+ if (res.status() === 404) {
+ expect(body).toContain("Assistant is disabled");
+ } else {
+ // If assistant is enabled, we just verify it didn't routing-404
+ expect(res.status()).not.toBe(404);
+ }
+ });
+
+ test("POST /holocron-api/chat without base prefix is rejected by Vite", async ({ request }) => {
+ // Vite intercepts non-prefixed routes and returns its own 404 with a
+ // helpful redirect hint. The client must always use the base-prefixed URL.
+ const res = await request.post("/holocron-api/chat", {
+ headers: { "content-type": "application/json" },
+ data: JSON.stringify({ message: "hello", modelMessages: [], currentSlug: "/" }),
+ });
+ expect(res.status()).toBe(404);
+ });
+});
+
test.describe("sitemap under base path", () => {
test("GET /docs/sitemap.xml returns valid sitemap", async ({ request }) => {
const res = await request.get("/docs/sitemap.xml");
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 01709ea6..0ab60ef0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,6 +101,34 @@ importers:
specifier: ^5.9.3
version: 5.9.3
+ example-basepath:
+ dependencies:
+ '@holocron.so/vite':
+ specifier: workspace:^
+ version: link:../vite
+ react:
+ specifier: ^19.2.5
+ version: 19.2.5
+ react-dom:
+ specifier: ^19.2.5
+ version: 19.2.5(react@19.2.5)
+ spiceflow:
+ specifier: 1.24.4-rsc.0
+ version: 1.24.4-rsc.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(zod@4.4.3)
+ vite:
+ specifier: ^8.0.10
+ version: 8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)
+ devDependencies:
+ '@types/react':
+ specifier: ^19.2.7
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
example-cloudflare:
dependencies:
'@holocron.so/vite':
@@ -121,7 +149,7 @@ importers:
devDependencies:
'@cloudflare/vite-plugin':
specifier: ^1.32.2
- version: 1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0(@cloudflare/workers-types@4.20260409.1))
+ version: 1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0)
'@cloudflare/workers-types':
specifier: ^4.20260408.0
version: 4.20260409.1
@@ -138,6 +166,31 @@ importers:
specifier: ^4.90.0
version: 4.90.0(@cloudflare/workers-types@4.20260409.1)
+ example-inside-next:
+ dependencies:
+ example-basepath:
+ specifier: workspace:^
+ version: link:../example-basepath
+ next:
+ specifier: ^16.2.6
+ version: 16.2.6(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react:
+ specifier: ^19.2.5
+ version: 19.2.5
+ react-dom:
+ specifier: ^19.2.5
+ version: 19.2.5(react@19.2.5)
+ devDependencies:
+ '@types/react':
+ specifier: ^19.2.7
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
integration-tests:
dependencies:
'@holocron.so/vite':
@@ -443,7 +496,7 @@ importers:
devDependencies:
'@cloudflare/vite-plugin':
specifier: ^1.32.2
- version: 1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0(@cloudflare/workers-types@4.20260409.1))
+ version: 1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0)
'@types/node':
specifier: ^25.6.0
version: 25.6.2
@@ -5261,7 +5314,7 @@ snapshots:
optionalDependencies:
workerd: 1.20260507.1
- '@cloudflare/vite-plugin@1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0(@cloudflare/workers-types@4.20260409.1))':
+ '@cloudflare/vite-plugin@1.32.2(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260507.1)(wrangler@4.90.0)':
dependencies:
'@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260507.1)
miniflare: 4.20260410.0
@@ -5768,8 +5821,7 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
- '@next/env@16.2.6':
- optional: true
+ '@next/env@16.2.6': {}
'@next/swc-darwin-arm64@16.2.6':
optional: true
@@ -6648,7 +6700,6 @@ snapshots:
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
- optional: true
'@tailwindcss/node@4.2.2':
dependencies:
@@ -7090,8 +7141,7 @@ snapshots:
base64-js@1.5.1: {}
- baseline-browser-mapping@2.10.28:
- optional: true
+ baseline-browser-mapping@2.10.28: {}
better-auth@1.6.10(@opentelemetry/api@1.9.0)(drizzle-orm@1.0.0-beta.21(@opentelemetry/api@1.9.0)(@types/mssql@9.1.11(@azure/core-client@1.10.1))(mssql@11.0.1(@azure/core-client@1.10.1))(sql.js@1.14.1)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@25.6.2)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))):
dependencies:
@@ -7170,8 +7220,7 @@ snapshots:
dependencies:
run-applescript: 7.1.0
- caniuse-lite@1.0.30001792:
- optional: true
+ caniuse-lite@1.0.30001792: {}
ccount@2.0.1: {}
@@ -7205,8 +7254,7 @@ snapshots:
dependencies:
clsx: 2.1.1
- client-only@0.0.1:
- optional: true
+ client-only@0.0.1: {}
cliui@9.0.1:
dependencies:
@@ -8580,7 +8628,6 @@ snapshots:
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- optional: true
nf3@0.3.16: {}
@@ -8700,7 +8747,6 @@ snapshots:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
- optional: true
postcss@8.5.14:
dependencies:
@@ -9135,7 +9181,6 @@ snapshots:
dependencies:
client-only: 0.0.1
react: 19.2.5
- optional: true
stylis@4.3.6: {}