-
Notifications
You must be signed in to change notification settings - Fork 0
Define Leadtype core and adapter boundary #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c70fde
45462ce
bd9b314
cf13ecf
b9f9fef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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 ( | ||
|
|
@@ -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" | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This recipe will not run. Next.js's App Router rejects a Move the handler to a sub-segment, e.g. |
||
|
|
||
| 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" | ||
|
|
@@ -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"; | ||
|
|
@@ -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" | ||
|
|
@@ -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" | ||
|
|
@@ -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: | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the documented Next App Router setup, this
route.tsis added in the sameapp/docs/[[...slug]]segment as the precedingpage.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 👍 / 👎.