Mount on Mars
The same Middleware instance that astra build crawls can be mounted on any Mars server. This is how you ship documentation alongside a live MoonBit application from one binary.
This page is a
.mbt.mdfile. Every```mbt checkblock below is compiled bymoon checkand executed bymoon test, so the snippets cannot drift away from the API they document.
Minimal mount
///|
test "mount astra on a Mars server" {
let cfg = @astra.SsgConfig::default()
let mw = @middleware.create(cfg, cwd=".")
let app = @mars.Server::new()
app.all("/*", mw.handler())
// `to_handler()` turns the server into a request handler. How you then
// listen is runtime-specific โ see the note below.
let _handler = app.to_handler()
}
Mars has no built-in listen: Server::to_handler() gives you an async (Request, Reader, ServerConnection) -> Unit, and the runtime adapter drives it. astra dev passes that handler to a Node FFI listener (astra/src/cli/dev.mbt); on Cloudflare Workers you would use @adapters.cloudflare_handler(app, env) instead.
Middleware::handler() returns a Mars Handler that responds to GET on every page URL the document tree exposes plus every entry in Middleware::asset_urls(). Middleware::list_urls() returns the union of those โ that is the same set the build crawler walks.
Mounting under a path prefix
To embed docs under /docs/*, register the middleware on the prefix instead of /*:
///|
test "mount astra under a path prefix" {
let cfg = @astra.SsgConfig::default()
let mw = @middleware.create(cfg, cwd=".")
let app = @mars.Server::new()
app.all("/docs/*", mw.handler()) // astra-served docs
let _handler = app.to_handler()
}
Configure base: "/docs/" in astra.config.json so links inside generated HTML point at the prefixed paths instead of the root.
What the middleware actually serves
For each request:
Look up the URL in the resolved page tree (
docs_dirwalk + i18n fallback).If hit, render the Markdown / MDX through the cached document pipeline and return HTML.
Otherwise, check the asset list (
/assets/*.css,/scripts/*.js, fonts, images). If hit, return the bundled bytes.Otherwise, return 404.
There is no separate static-asset middleware to wire up. Everything astra needs to render the docs is enumerated by list_urls() and served by handler().
Build parity
astra build is a thin loop over the same two methods โ no server, no socket:
///|
test "build is a loop over list_urls and render_url" {
let cfg = @astra.SsgConfig::default()
let mw = @middleware.create(cfg, cwd=".")
for url in mw.list_urls() {
let res = mw.render_url(url)
// `astra build` writes `res.body` to `<out>/<url>/index.html`.
assert_true(res.status > 0)
assert_true(res.content_type != "")
}
}
This is why dev and build always agree. If you find a divergence, treat it as a bug โ see astra/docs/parity-notes.md for known gaps (utility CSS extraction, sitemap generation, Cloudflare adapter manifests).
Hot reload
astra dev watches docs_dir and bumps the document tree's revision on change. The middleware sees the new revision on the next request and re-renders. There is no MoonBit recompile in the dev path โ Markdown changes are reflected as fast as the renderer runs.