MDAN MDAN Docs
官网

服务端运行时

@mdanai/sdk/server 是在服务端组织 MDAN 应用时的 TypeScript 主入口。

它按 MDAN 页面里显式写出的 HTTP 路径注册和处理操作。

MDAN 现在官方支持 Node 和 Bun,而且两边用的是同一套服务端模型:

变化的是最外层 host 适配器,不是页面和 action 的应用模型。

基本用法

import { composePage } from "@mdanai/sdk/core";
import { createHostedApp } from "@mdanai/sdk/server";

const server = createHostedApp({
  pages: {
    "/guestbook": pageHandler
  },
  actions: [
    {
      target: "/list",
      methods: ["GET"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: listHandler
    },
    {
      target: "/post",
      methods: ["POST"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: postHandler
    }
  ]
});

Handler 形状

handler 会收到一个 context 对象,包含:

如果你直接使用 createHostedApp(),action handler 还会拿到:

最常见的 block 操作可以直接写成:

const page = composePage(source, {
  blocks: {
    guestbook: "## 2 live messages\n\n- Welcome\n- Hello"
  }
});

const server = createHostedApp({
  pages: {
    "/guestbook": () => page
  },
  actions: [
    {
      target: "/list",
      methods: ["GET"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: ({ block }) => block()
    }
  ]
});

运行时会把这个结果序列化成可直接返回的 Markdown 片段。

createHostedApp() 不会通过“先渲染一次页面再反推操作”来猜 action 绑定。actions 必须显式声明 target / methods / routePath / blockName,这样注册关系才稳定,不会被页面当前显示内容偷偷影响。

当你的应用天然就是“一组页面 + 一组 actions”时,优先用 createHostedApp()。只有在你需要完全手动控制时,再退到 createMdanServer()。

请求桥接

如果你想完全自己接框架,适配层只需要把中立请求对象交给 server.handle():

const response = await server.handle({
  method: "POST",
  url: "https://example.com/login",
  headers: {
    accept: "text/markdown",
    "content-type": "text/markdown"
  },
  body: 'nickname: "guest", message: "hello"',
  cookies: {}
});

返回对象包含:

如果你运行在 Node http 上,直接用 Node 适配器:

import { createHost } from "@mdanai/sdk/server/node";

http.createServer(
  createHost(server, {
    rootRedirect: "/guestbook",
    transformHtml: injectEnhancement,
    staticFiles: {
      "/starter/client.js": join(exampleRoot, "dist", "client.js")
    },
    staticMounts: [{ urlPrefix: "/sdk/", directory: join(repoRoot, "sdk") }]
  })
);

如果你运行在 Bun 上,直接用 Bun 适配器:

import { createHost } from "@mdanai/sdk/server/bun";

Bun.serve({
  port: 3000,
  fetch: createHost(server, {
    rootRedirect: "/guestbook",
    transformHtml: injectEnhancement
  })
});

运行时入口

页面和 action 逻辑继续使用共享服务端运行时:

import { createHostedApp } from "@mdanai/sdk/server";

然后再选择与你部署环境一致的 host 适配器:

import { createHost } from "@mdanai/sdk/server/node";
import { createHost } from "@mdanai/sdk/server/bun";

内置职责

@mdanai/sdk/server 已经负责:

自定义 Markdown 渲染器

当浏览器走 HTML 链路时,@mdanai/sdk/server 会负责把 Markdown 渲染成 HTML。这个能力支持注入:

const server = createHostedApp({
  markdownRenderer: {
    render(markdown) {
      return marked.parse(markdown);
    }
  },
  pages,
  actions
});

如果你同时使用默认 @mdanai/sdk/elements UI,建议把同一个 markdownRenderer 对象也传给 mountMdanElements(...),这样服务端和默认 UI 的 Markdown 呈现会保持一致。

什么时候包一层适配器

如果你后面想接 Express、Hono 或 Next,建议围绕 server.handle() 做一层很薄的适配器,而不是分叉运行时逻辑。