Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/leadtype-next-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"leadtype": minor
---

Add `leadtype/next` framework adapter and formalize the core/adapter boundary.

`leadtype/next` exposes three server-only helpers for Next.js App Router: `createGenerateStaticParams(...)`, `createLoadPageData(...)`, and `createDocsRouteHandler(...)`. The route handler wraps `createAgentMarkdownResponse` so a docs app can serve raw markdown, handle `Accept: text/markdown` negotiation, and detect AI user agents from a one-line `route.ts`. The companion `leadtype/next/client` subpath exports a `useLeadtypeSearch` React hook plus a framework-free `createSearchClient` factory that lazy-loads `search-index.json` / `search-content.json` and runs BM25 per keystroke.

`react` is now an optional peer dependency for `leadtype/next/client`. Server-only consumers never pull in React.

Documents the core/adapter boundary in a new `docs/reference/architecture` page: leadtype core has zero framework runtime deps, adapters live at flat `leadtype/<framework>` subpaths, and **no leadtype package — core or adapter — ever ships rendered DOM**. State primitives (hooks, composables, stores, handler factories) are allowed; `<SearchBox>`-style components are not. The docs also name the planned native adapter shapes for Nuxt, SvelteKit, Astro, TanStack Start, Vue search, and Svelte search without exporting those APIs yet. The boundary is now enforced by tests in `packages/leadtype/src/internal/package-surface.test.ts` that scan import graphs and fail if framework runtimes leak into core or one adapter imports from another.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ flowchart LR
bundle_out --> offline_agents
```

leadtype is **not a docs website framework**. Bring your own UI — Next.js, TanStack Start, Astro, anything — and let leadtype handle conversion, validation, search, and the agent-facing outputs that website frameworks don't ship.
leadtype is **not a docs website framework**. Bring your own UI — Next.js, TanStack Start, Astro, Nuxt, SvelteKit, Vue, Svelte, anything — and let leadtype handle conversion, validation, search, and the agent-facing outputs that website frameworks don't ship.

## Choose your path

Expand Down Expand Up @@ -63,6 +63,7 @@ Full docs at [leadtype.dev](https://leadtype.dev/docs):
- [Add search](https://leadtype.dev/docs/build/add-search)
- [Frontmatter](https://leadtype.dev/docs/authoring/frontmatter)
- [CLI reference](https://leadtype.dev/docs/reference/cli)
- [Architecture](https://leadtype.dev/docs/reference/architecture) — core package boundary and framework adapter rules
- [Methodology](https://leadtype.dev/docs/methodology) — how leadtype differs from Fumadocs, Starlight, and Mintlify

## Repo layout
Expand Down
4 changes: 4 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 53 additions & 7 deletions docs/build/use-the-source-primitive.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Plus an MDX integration for your bundler (`@next/mdx`, `@astrojs/mdx`, `@mdx-js/

### Next App Router

The `leadtype/next` adapter wraps the common Next wiring so the page, route handler, and search hook each become a one-line export. Use it as the recommended path; fall back to calling `createDocsSource()` directly only when you need behavior the adapter doesn't expose.

```ts title="next.config.mjs"
import createMDX from "@next/mdx";
import { createMdxSourcePlugins } from "leadtype/mdx";
Expand All @@ -57,14 +59,20 @@ export default createMDX({
```tsx title="app/docs/[[...slug]]/page.tsx"
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import {
createGenerateStaticParams,
createLoadPageData,
} from "leadtype/next";
import { source } from "@/lib/source";
import { mdxComponents } from "@/lib/mdx-components";

const loadPageData = createLoadPageData({ source });
export const generateStaticParams = createGenerateStaticParams({ source });

export default async function DocsPage({
params,
}: { params: Promise<{ slug?: string[] }> }) {
const { slug = [] } = await params;
const page = await source.loadPage(slug);
const page = await loadPageData((await params).slug);
if (!page) notFound();

return (
Expand All @@ -75,13 +83,21 @@ export default async function DocsPage({
</article>
);
}
```

export async function generateStaticParams() {
const pages = await source.listPages();
return pages.map((page) => ({ slug: page.slug }));
}
Add a sibling `route.ts` to serve raw markdown and content negotiation:

```ts title="app/docs/[[...slug]]/route.ts"
Comment on lines +88 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don’t document a sibling route.ts for the docs page

In the documented Next App Router setup, this route.ts is added in the same app/docs/[[...slug]] segment as the preceding page.tsx, but Next does not allow a route handler and a page at the same route segment level. Consumers following the recommended adapter path will hit a routing/build error before markdown negotiation can work; the handler needs to be mounted at a different segment or implemented via middleware/proxy instead.

Useful? React with 👍 / 👎.

import { createDocsRouteHandler } from "leadtype/next";
import manifest from "@/generated/agent-readability.json" with { type: "json" };

export const GET = createDocsRouteHandler({
manifest: { ...manifest, version: 1 } as const,
});
```
Comment on lines +88 to 97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recipe will not run. Next.js's App Router rejects a route.ts at the same segment as page.tsxnext dev and next build error out with a route conflict (docs: "There cannot be a route.js file at the same route segment level as page.js", https://nextjs.org/docs/app/getting-started/route-handlers).

Move the handler to a sub-segment, e.g. app/docs/[[...slug]]/raw/route.ts, and update the JSDoc example in packages/leadtype/src/next/index.ts to match.


Pair with `useLeadtypeSearch` from `leadtype/next/client` when building a search input — see the [search recipe](/docs/build/add-search).

### Astro Content Collections

```ts title="astro.config.mjs"
Expand Down Expand Up @@ -190,7 +206,31 @@ function DocsCatchAllRoute() {

Generate `docs-pages.json` at build time by calling `createDocsSource().listPages()` from a build script and writing each page's `slug`, `urlPath`, and `globKey` (path relative to the catch-all route, POSIX separators).

### Vite + `@mdx-js/rollup` (works for Vue, Solid, Svelte starters)
There is no `leadtype/tanstack-start` export yet. The future adapter should wrap this same shape with TanStack Router route helpers, server functions, and React state helpers. TanStack AI answer streaming already lives separately under `leadtype/search/tanstack`.

### Vue + Vite

Use a Vue content or MDX plugin that exposes a unified/remark plugin list, then pass Leadtype's source preset into that plugin. Leadtype supplies the transforms, route/search data, and tag prop contracts; your Vue app owns rendering.

```ts title="vite.config.ts"
import vue from "@vitejs/plugin-vue";
import { createMdxSourcePlugins } from "leadtype/mdx";
import { defineConfig } from "vite";

const leadtypeRemarkPlugins = createMdxSourcePlugins();

export default defineConfig({
plugins: [
vue(),
// Add `leadtypeRemarkPlugins` to your Vue MDX/content plugin's
// remark/unified plugin list.
],
});
```

For client search today, fetch `/docs/search-index.json` and `/docs/search-content.json`, then call `searchDocs()` from `leadtype/search`. A future `leadtype/search/vue` helper should expose the same behavior as a Vue composable.

### Vite + `@mdx-js/rollup` (React, Preact, Solid, or custom JSX runtimes)

```ts title="vite.config.ts"
import mdx from "@mdx-js/rollup";
Expand All @@ -204,6 +244,8 @@ export default {
};
```

> Nuxt, SvelteKit, Astro, TanStack Start, Vue, and Svelte recipes wire `createDocsSource()` manually today. Dedicated `leadtype/nuxt`, `leadtype/sveltekit`, `leadtype/astro`, `leadtype/tanstack-start`, `leadtype/search/vue`, and `leadtype/search/svelte` adapters with native handler factories and state primitives are tracked under issue [#41](https://github.com/inthhq/leadtype/issues/41) and the search-helper follow-up [#45](https://github.com/inthhq/leadtype/issues/45).

### Nuxt

```ts title="nuxt.config.ts"
Expand All @@ -215,6 +257,8 @@ export default defineNuxtConfig({
});
```

Use Nitro routes for generated markdown and Agent Readability artifacts. For search, load the generated JSON files from a Vue composable and pass them to `searchDocs()` from `leadtype/search`; a future `leadtype/nuxt` adapter should make that composable copy-paste small without owning UI.

### SvelteKit + `mdsvex`

```ts title="svelte.config.js"
Expand All @@ -227,6 +271,8 @@ export default {
};
```

Use `+page.server.ts` to call `source.loadPage(slug)`, `entries()` to prerender known routes from `source.listPages()`, and `+server.ts` for markdown or JSON artifact responses. For search, load generated JSON into a store or rune and call `searchDocs()`; a future `leadtype/search/svelte` helper should keep that state primitive UI-free.

### Pattern for any other host

If your framework's MDX integration accepts a remark plugin list, leadtype works. Three pieces every time:
Expand Down
1 change: 1 addition & 0 deletions docs/docs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineDocsConfig({
{ urlPath: "/docs/build/add-search" },
{ urlPath: "/docs/build/optimize-docs-for-agents" },
{ urlPath: "/docs/package-docs/bundle" },
{ urlPath: "/docs/reference/architecture" },
],
agentGuidance:
"Open /docs/llms.txt to route the task, then use /llms-full.txt only when page-level markdown is not enough.",
Expand Down
6 changes: 3 additions & 3 deletions docs/methodology.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ Choose a website framework when the main job is publishing a polished docs UI qu
- The **MDX tag contract** — typed prop shapes for `<Callout>`, `<Tabs>`, `<Steps>`, `<TypeTable>`, and the rest of the custom tags (see [`leadtype/mdx`](/docs/reference/mdx)).
- A **build-time source preset** that expands `<include>` partials, resolves `<ExtractedTypeTable>`, and strips authoring `import`s — without flattening live components.
- A **framework-neutral source primitive** (`createDocsSource`) exposing navigation, page loading, search index building, and include resolution.
- A thin **fumadocs adapter** (`leadtype/fumadocs`) wiring the source into fumadocs's `Source` interface.
- **Framework adapters and recipes** — thin, optional packages or copyable recipes that wire the source primitive into native host conventions: Next App Router, Fumadocs, Nuxt/Nitro, SvelteKit, Astro, TanStack Start, Vue, and Svelte. Adapters export **state and routing primitives only** — hooks, composables, stores, route-handler factories, static-param helpers, pure data helpers — never rendered DOM.
- The agent / LLM pipeline: MDX-to-markdown conversion, `llms.txt`, root `llms-full.txt` fallback context, markdown mirrors, sitemap, and AGENTS.md bundles.
- A static, edge-safe **search index** plus optional source-grounded answer streaming.
- **Lint rules** for frontmatter, navigation metadata, and internal links.
- **CLI orchestration** so the whole pipeline runs from one command.

## What leadtype does not own

- Visual UI, theming, or component styling.
- Runtime component implementations — your docs app implements components against the tag contract (see [Components](/docs/authoring/components) and [`leadtype/mdx`](/docs/reference/mdx)).
- **Rendered DOM, ever.** No leadtype package — core or adapter — ships `<SearchBox>`, `<DocsLayout>`, `<Callout>`, `<Sidebar>`, or any component that emits markup. This is permanent, not a "for now." Consumers implement components against the tag contract from [`leadtype/mdx`](/docs/reference/mdx); leadtype supplies the data, types, and wiring. The one-line test for any new adapter: *does it return data, state, or a function, or does it return JSX?* If JSX, it doesn't ship.
- Visual UI, theming, CSS, design tokens, or component styling.
- Routing, hosting, deployment, or analytics.
- The MDX compiler — leadtype produces the source preset; your bundler (Next App Router, Vite, fumadocs-mdx, Astro) does the compile.

Expand Down
92 changes: 92 additions & 0 deletions docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
title: "Architecture"
description: "The core / adapter boundary — what ships where, and the rules adapters must follow."
group: reference
---

# Architecture

Leadtype is split into a **framework-neutral core** and a small set of **thin adapters** that wire the core into specific host frameworks. The rules below are the contract — they tell you where new code lives, what it can import, and what it can never ship.

## The three layers

### Core

| Subpath | Purpose |
| --- | --- |
| `leadtype` | `createDocsSource`, `defineDocsConfig`, agent-readability types. |
| `leadtype/mdx` | Tag types (`CalloutProps`, `TabsProps`, …) and `createMdxSourcePlugins()`. |
| `leadtype/remark` | Remark plugins (include, type-table, agent flattening). |
| `leadtype/convert` | MDX → markdown conversion. |
| `leadtype/llm` | `llms.txt`, `llms-full.txt`, sitemap, AGENTS.md, agent-readability manifest. |
| `leadtype/llm/readability` | Runtime helpers (`createAgentMarkdownResponse`, content negotiation). |
| `leadtype/search` | Static BM25 index + edge-safe runtime. |
| `leadtype/search/{node,vercel,tanstack,cloudflare,ai,bash}` | Host-runtime adapters for the search/answer pipeline. |
| `leadtype/lint` | Frontmatter, navigation, and link validation. |

Core has **zero framework runtime dependencies**. It imports no React, Vue, Svelte, Next, Nuxt, SvelteKit, Astro, or Solid code. Anything published under one of the subpaths above must stay framework-neutral.

### Host-runtime adapters (`leadtype/search/*`)

The `leadtype/search/*` subpaths are scoped by **where the code runs** — Node, Vercel Edge, Cloudflare Workers, TanStack runtime, the bash tool — not by which UI framework wraps them. They cover the answer-streaming/search read path on their target runtime.

### Framework adapters

| Subpath | Host | What it exports |
| --- | --- | --- |
| `leadtype/fumadocs` | fumadocs-core | `fumadocsSource()` — maps `DocsSource` to fumadocs's `Source` interface. |
| `leadtype/next` | Next.js App Router (server) | `createGenerateStaticParams`, `createLoadPageData`, `createDocsRouteHandler`. No React. |
| `leadtype/next/client` | Next.js App Router (client) | `useLeadtypeSearch` React hook, `createSearchClient` vanilla factory. |

Framework adapters are thin. They wire the core primitives into the host's native conventions and declare their host package as an **optional peer dependency**. A consumer that doesn't use fumadocs never installs fumadocs-core; a consumer that doesn't use Next never installs React.

## Planned adapter shapes

The boundary applies to every framework, including adapters that are not exported yet:

| Future subpath | Native host shape |
| --- | --- |
| `leadtype/nuxt` | Nitro route helpers, build-time source wiring, and Vue composables. |
| `leadtype/sveltekit` | `+page.server.ts`, `+server.ts`, `entries()`, and Svelte stores/runes. |
| `leadtype/astro` | `getStaticPaths()`, endpoint helpers, Content Collections interop, and island-friendly client helpers. |
| `leadtype/tanstack-start` | TanStack Router route helpers, static route manifests, server functions, and React state helpers. |
| `leadtype/search/vue` | Vue composables over generated `search-index.json` and `search-content.json`. |
| `leadtype/search/svelte` | Svelte stores/runes over generated search artifacts. |

These are future public surfaces, not hidden exports. Until they ship, use the recipes in [Use the source primitive](/docs/build/use-the-source-primitive), which wire the same core primitives into each framework directly.

`leadtype/tanstack-start` is intentionally separate from `leadtype/search/tanstack`: the former would adapt docs routing and source data to TanStack Start, while the latter is the existing TanStack AI answer-streaming runtime.

## Rules

### Flat naming

Adapters live under `leadtype/<framework>`. There is no `leadtype/adapters/*` nesting. Sub-runtimes (server vs. client, hook vs. handler) use a second segment: `leadtype/next`, `leadtype/next/client`.

### No rendered DOM

**No leadtype package — core or adapter — emits rendered DOM. Ever.** Adapters ship state primitives (hooks, composables, stores), route-handler factories, and pure data helpers. Anything that renders markup — `<SearchBox>`, `<DocsLayout>`, `<Sidebar>`, theme tokens, CSS — does not ship under the `leadtype` name. Consumers implement components against the tag contract from [`leadtype/mdx`](/docs/reference/mdx); leadtype supplies the data and the wiring.

When reviewing a new adapter PR, ask: *does this return data, state, or a function, or does it return JSX?* If JSX, the PR doesn't ship.

### Dependency rule

An adapter at `leadtype/<framework>` may import:

1. Other leadtype core entry points (`leadtype`, `leadtype/search`, `leadtype/llm/readability`, etc.).
2. Its **declared optional peer** (e.g. `leadtype/fumadocs` may import `fumadocs-core`; `leadtype/next/client` may import `react`).

Adapters may **not** import from another adapter — `leadtype/next` never reaches into `leadtype/fumadocs`. They compose with core, not each other.

### Enforcement

The contract above is enforced by tests in `packages/leadtype/src/internal/package-surface.test.ts`. The suite scans every file under `src/`, fails if a framework runtime import leaks into a non-adapter directory, and fails if one adapter directory imports from another.

## Adding a new framework adapter

1. Pick the subpath: `leadtype/<framework>` (e.g. `leadtype/nuxt`, `leadtype/sveltekit`, `leadtype/astro`, `leadtype/tanstack-start`). Split into `<framework>` (server) and `<framework>/client` (or `<framework>/composables`, etc.) only when the framework has distinct server/client runtimes that share no peer dependency.
2. Declare the host package and any framework UI runtime (`vue`, `svelte`, `solid-js`) as **optional peer dependencies** in `packages/leadtype/package.json`. Mark each one optional in `peerDependenciesMeta`.
3. Add the entry to `rollup.config.ts` `entries` and to `package.json` `exports`.
4. Update the entry-point list in `package-surface.test.ts` so the test enforces the new subpath.
5. Export **state and routing primitives only** — no rendered DOM.
6. Add tests covering handler factories with synthetic `Request` objects and state primitives with a stub backing implementation. Don't require a running framework runtime in unit tests.
5 changes: 5 additions & 0 deletions packages/leadtype/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ Full docs at [leadtype.dev](https://leadtype.dev/docs). Highlights:
| `leadtype/search/tanstack` | TanStack AI answer streaming. |
| `leadtype/search/cloudflare` | Cloudflare AI Gateway / Workers AI adapter. |
| `leadtype/lint` | `lintDocs` and the `leadtype lint` CLI. |
| `leadtype/fumadocs` | Adapter mapping `createDocsSource()` to fumadocs's `Source` interface. |
| `leadtype/next` | Next.js App Router server adapter — `createDocsRouteHandler`, `createGenerateStaticParams`, `createLoadPageData`. |
| `leadtype/next/client` | Next.js client hook — `useLeadtypeSearch` and the framework-free `createSearchClient`. |

Framework adapters are thin and ship **state and routing primitives only** — no rendered DOM. See the [architecture reference](https://leadtype.dev/docs/reference/architecture) for the boundary contract and how to add more frameworks (`leadtype/nuxt`, `leadtype/sveltekit`, `leadtype/astro`, `leadtype/tanstack-start`, `leadtype/search/vue`, and `leadtype/search/svelte` are tracked under [#41](https://github.com/inthhq/leadtype/issues/41) / [#45](https://github.com/inthhq/leadtype/issues/45)).

The `leadtype` binary wraps `generate` and `lint`. Use the library entry points when you need custom plugin order, base URL precedence, or alternate output paths.

Expand Down
Loading
Loading