Application Structure
This page is about how to lay out code, pages, and interaction when you build a real MDAN app, especially agent apps and skills apps.
The short version is: keep page source, server logic, and browser-side code separate, but do not split them into something heavier than the app itself.
Recommended Shape
The smallest useful structure is usually:
app/*.md: page sourceapp/server.ts: page composition and action handlersapp/client.ts: browser runtime and UI mountingindex.mjs: local runtime host entry
You can think of that as three layers:
app/*.mddefines page content and operationsapp/server.tsconnects pages to real application stateapp/client.tshandles follow-up interaction in the browser
index.mjs only hosts the app so you can run it locally or deploy it in Node or Bun.
Keep Responsibilities Clear
The easiest way to make an MDAN app hard to maintain is to mix responsibilities.
A clean split usually looks like this:
- Markdown defines content and operations
composePage()attaches runtime block content to the pagecreateHostedApp({ pages, actions })registers pages and operations as an appcreateHost()from the runtime adapter hosts that app in Node or BuncreateHeadlessHost()handles follow-up interaction in the browser
That way, the page layer, the server layer, and the browser layer each do one thing.
How Pages and Operations Line Up
MDAN uses explicit page routes and explicit action paths.
- page route:
pages["/docs"] = () => composedPage - action path: each action declares
target + methods + routePath + blockName
That keeps the relation between pages and interaction stable. You do not need to infer bindings from whatever the page happens to look like right now.
In practice:
routePathsays which page an operation belongs toblockNamesays which part of the page it primarily updatestargetsays which HTTP path the request actually hits
When those three line up, behavior tends to stay predictable.
Where HTML Shell Logic Belongs
Shared HTML shell logic should usually live in server-side wrapping:
- use
renderHtmlto build the global shell - or use
transformHtmlin the runtime host entry for final injection
Typical responsibilities look like this:
- Markdown: content and interaction
renderHtml: headers, navigation, theme, and other global shell concerns- browser runtime: follow-up page and block updates
So the Markdown page is the application itself, while the HTML shell wraps it into a fuller website or page experience.
How To Organize Operations
Each action should explicitly declare:
targetmethodsroutePathblockNamehandler
The two most common cases are:
Read Action (GET)
handler: ({ block }) => block()
Write Action (POST)
handler: ({ inputs, block }) => {
// update domain state
return block();
}
If all you need is to refresh the current block, return block().
If a write also needs to carry new state, an error, or the next available operation, keep that with the returned fragment instead of scattering it elsewhere.
Recommended Build Order
- Decide your route list and page files.
- Write the Markdown for each page, then build a
renderPage()-style composition function. - Register each page operation as an explicit action.
- Wire up the runtime entry, static assets, and HTML shell.
- Add the browser runtime and verify local updates and page transitions.
Common Pitfalls
- treating the example shell import map as public SDK API
- returning a full page when a block fragment is enough
- putting domain state updates into the UI layer instead of action handlers
- changing operation definitions in the page without updating
target,routePath, andblockNameon the server side