Leadtype
Reference

leadtype/mdx

The leadtype/mdx subpath is the consumer-facing MDX surface — everything you need to compile leadtype-authored MDX in your own renderer (Next App Router, TanStack Start, Nuxt, Astro content collections, SvelteKit, or fumadocs).

It exports three things:

  1. Tag type contracts — typed prop shapes for every custom MDX tag.
  2. createMdxSourcePlugins() / mdxSourcePlugins — a remark preset that performs build-time resolution only (expand includes, resolve <ExtractedTypeTable>, strip authoring imports) and leaves every other custom tag as JSX.
  3. Include-resolution helpersresolveInclude, parseIncludeSpecifier, extractMdxSection for direct use.

Pair it with leadtype/markdown (markdown flattening for the agent/LLM pipeline) — they are sibling surfaces, not alternatives. Most projects use both.

The MDX-source preset

import createMDX from "@next/mdx";
import { createMdxSourcePlugins } from "leadtype/mdx";
import path from "node:path";

const typeTableBasePath = path.resolve(process.cwd(), ".c15t");

const withMDX = createMDX({
  options: {
    remarkPlugins: [
      ...createMdxSourcePlugins({ typeTableBasePath }),
    ],
  },
});

Use the mdxSourcePlugins constant only when the default path inference is enough for your project. When <ExtractedTypeTable path="./packages/..." /> should resolve from a known source root (.c15t, .leadtype, the repo root, or a docs folder), prefer createMdxSourcePlugins({ typeTableBasePath }). If you intentionally keep type files inside docs/, pass path.resolve(process.cwd(), "docs").

The preset is intentionally minimal — only the transforms that must run at build time:

PluginPurpose
remarkIncludeExpands <include src="…" />, <import>, <include-c15t>
remarkResolveTypeTableJsx<ExtractedTypeTable name path /><TypeTable properties={…} />
remarkResolveDocPlaceholdersResolves {{...}} placeholders against doc context
remarkRemoveImportsStrips authoring import statements that aren't needed at runtime

Every other custom tag (<Callout>, <Tabs>, <Steps>, <Mermaid>, …) stays JSX so your runtime components render them.

Framework-neutral by design

Tag types describe the author surface — the attributes that appear on a tag in source MDX. They deliberately do not import from any UI framework: by default children is typed as unknown.

Register your renderer's child type once per project by augmenting ChildrenTypeRegistry, and every tag's children becomes that type — no casts, no per-component wrappers:

types.d.ts
declare module "leadtype/mdx" {
  interface ChildrenTypeRegistry {
    type: import("react").ReactNode; // or your framework's child type
  }
}
// The export marker makes this file a module, so the declaration *augments*
// the package instead of replacing its types.
export {};

The same shape works in React, Vue, Svelte, Solid, Astro, Qwik, or anything else with an MDX compiler that accepts remark plugins.

React

With the registry augmentation above in place:

import type { CalloutProps } from "leadtype/mdx";

export function Callout({ variant, title, children }: CalloutProps) {
  return (
    <aside data-variant={variant ?? "info"}>
      {title ? <strong>{title}</strong> : null}
      <div>{children}</div>
    </aside>
  );
}

To add framework-native attributes (className, event handlers), intersect as before: CalloutProps & HTMLAttributes<HTMLElement>.

Vue

<script setup lang="ts">
import type { CalloutProps } from "leadtype/mdx";

defineProps<Omit<CalloutProps, "children">>();
</script>

<template>
  <aside :data-variant="variant ?? 'info'">
    <strong v-if="title">{{ title }}</strong>
    <div><slot /></div>
  </aside>
</template>

Svelte

<script lang="ts">
  import type { CalloutProps } from "leadtype/mdx";
  import type { Snippet } from "svelte";

  let {
    variant = "info",
    title,
    children,
  }: Omit<CalloutProps, "children"> & { children?: Snippet } = $props();
</script>

<aside data-variant={variant}>
  {#if title}<strong>{title}</strong>{/if}
  <div>{@render children?.()}</div>
</aside>

Solid

import type { CalloutProps } from "leadtype/mdx";
import type { JSX } from "solid-js";

type SolidCalloutProps = Omit<CalloutProps, "children"> & {
  children?: JSX.Element;
};

export function Callout(props: SolidCalloutProps) {
  return (
    <aside data-variant={props.variant ?? "info"}>
      {props.title ? <strong>{props.title}</strong> : null}
      <div>{props.children}</div>
    </aside>
  );
}

Astro

---
import type { CalloutProps } from "leadtype/mdx";
type Props = Omit<CalloutProps, "children">;
const { variant = "info", title } = Astro.props as Props;
---

<aside data-variant={variant}>
  {title && <strong>{title}</strong>}
  <div><slot /></div>
</aside>

Full type inventory

import type {
  // Layout
  AudienceProps,
  AudienceTarget,
  SectionProps,
  DetailsProps,

  // Callouts
  CalloutProps,
  CalloutVariant,
  CalloutTypeAlias,

  // Navigation / structure
  TabsProps,
  TabProps,
  StepsProps,
  StepProps,
  AccordionProps,
  AccordionItemProps,

  // Cards / topics
  CardsProps,
  CardProps,
  CardVariant,
  TopicSwitcherProps,
  TopicSwitcherItem,

  // File tree
  FileTreeProps,
  FolderProps,
  FileProps,

  // Code / commands
  CommandTabsProps,
  CommandMode,
  PackageManager,
  ExampleProps,
  ExampleSourceFile,
  PromptProps,

  // Type tables
  TypeTableProps,
  TypeTableProperty,
  ExtractedTypeTableProps,

  // Diagrams
  MermaidProps,
} from "leadtype/mdx";

Every prop name is part of the 1.0 contract — bumping a shape is a breaking change. New optional props are minor.

Build-time only: <ExtractedTypeTable>

<ExtractedTypeTable name="X" path="..." /> is an authoring convenience. The source preset replaces it at build time with <TypeTable properties={…extracted} />, so consumers only implement the runtime <TypeTable> component:

import type { TypeTableProps } from "leadtype/mdx";

export function TypeTable({ properties, title, description }: TypeTableProps) {
  return (
    <section>
      {title ? <h3>{title}</h3> : null}
      {description ? <p>{description}</p> : null}
      <table>
        {Object.entries(properties).map(([name, prop]) => (
          <tr key={name}>
            <td><code>{name}</code></td>
            <td><code>{prop.type}</code></td>
            <td>{prop.description}</td>
          </tr>
        ))}
      </table>
    </section>
  );
}

<ExtractedTypeTable> requires typescript to be installed as an optional peer dep in your docs project. Failed extraction emits a visible warning by default; pass typeTableStrict: true to createMdxSourcePlugins() when a missing type should fail the build.

Include resolution

resolveInclude reads + classifies an include target without going through remark — useful when loading a partial outside of the bundler pipeline:

import { resolveInclude } from "leadtype/mdx";

const result = await resolveInclude("./shared/install.mdx#bun", {
  fromDir: process.cwd(),
});

if (result.kind === "markdown") {
  console.log(result.content); // frontmatter-stripped body
  console.log(result.section); // "bun"
} else {
  console.log(result.lang, result.content); // code-block form
}

parseIncludeSpecifier(specifier) and extractMdxSection(root, id) are exposed for callers that want to compose their own pipeline.

Re-exported path helpers

Routing primitives that previously lived under leadtype/internal are re-exported for consumer use:

import {
  type DocsPathMount,
  normalizeBaseUrl,
  normalizeDocsPath,
  normalizeUrlPrefix,
  stripDocsExtension,
  toDocsUrlPath,
} from "leadtype/mdx";