npx skills add ...
npx skills add svallory/marko-ui --skill marko-run
Use when building or editing a Marko Run (@marko/run) app — anything under src/routes (+page, +layout, +handler, +middleware, +meta, +404, +500), route path syntax ($id, $$rest, _pathless, flat routes), Run.GET/POST verb helpers, ctx/$global request context, validation (params/search/json/form), data loading via next(), Run.href, vite.config adapters (node/static/netlify), or the marko-run CLI. Not for Marko template syntax itself (see marko6).
npx skills add svallory/marko-ui --skill marko-run
Distilled from https://markojs.com/docs/marko-run. File-based routing on Vite, streaming SSR, zero config. references/routing.md (files, paths, flat routes, execution order), references/handlers.md (verb helpers, validation, data loading, context, typed URLs, embedding), references/deploy.md (vite plugin, adapters, CLI).
src/routes/** — only +-prefixed files are routable; everything else is ignored (colocate components/tests freely).ctx (handlers/middleware) === $global (templates). ctx.params, ctx.url are serialized to the browser by default; add others via ctx.serializedGlobals.next({ ... }) → ctx.data / $global.data (shallow merge). Pass promises, <await> them in the page — awaiting before next kills streaming.Run is a global (no import): Run.GET/HEAD/POST/PUT/DELETE/PATCH/OPTIONS/QUERY/ALL, Run.href, types Run.Context, Run.Platform.| File | Role |
|---|---|
+page.marko | GET → HTML. One per path. |
+layout.marko | wraps nested layouts/pages; input.content = child. $global = ctx. |
+handler.ts | export const GET/POST/... = Run.X(...). One per path. Return Response, or undefined → next() auto. Missing HEAD → GET minus body. |
+middleware.ts | export default Run.ALL(...) (all verbs) or Run.POST(...) (one verb); runs before handlers; options-only form Run.ALL({ search }) attaches validation below. |
+meta.json / +meta.ts | static ctx.meta; verb keys ("POST": {...}) shallow-override per method. |
+404.marko, +500.marko | root-only; when Accept has text/html; wrapped by root layout. |
users static · _admin pathless (ignored in URL, scopes layouts/middleware) · $id param ($ alone = match, no capture) · $$rest catch-all to end ($$ no capture; nothing nests inside).
Flat routes: projects.$projectId.members+page.marko; dirs may use it too (projects.$projectId/members+page.marko); merged with nested dirs. Multi-path members,people+page.marko; groups (members,people); optional (home,) or (home,_x). Escape control chars with backticks: `sitemap.xml`/+handler.ts.
params, search (fn or sync Standard Schema; lazy, cached on first access), json (validator, maxBytes 1MiB), form (validator, maxBytes, maxParts 1000, maxFiles 20, maxFileBytes 1MiB, onFile(ctx, file); repeated fields → arrays). Forms: Run.POST(options, [auth, handler]) arrays compose.ctx.body is a promise (await ctx.body), lazy, undefined when no json/form option. Don't call ctx.request.json() — bypasses validation/limits.ctx methods: fetch(url, init) (internal re-request, sets parent), render(template, input, init), redirect(to, status=302), back(fallback="/", status).Run.href("/projects/$projectId/members", { params: { projectId: 42 }, search: {...}, hash }) — typed against real routes when .marko-run/routes.d.ts is in tsconfig include; rewritten at compile time on the client.Generated .marko-run/routes.d.ts (needs tsconfig at root; add to include) narrows Run.Context per file (params, meta, search, body, upstream data). Extend globally: declare module "@marko/run" { interface Context { db: DB } interface Platform { ... } } — reserve for app-wide props; per-route data goes through next().
vite.config needed. When needed: import marko from "@marko/run/vite"; plugins: [marko({ adapter, routesDir, trailingSlashes })] (+ all @marko/vite options). trailingSlashes: RedirectWithout (default) | RedirectWith | RewriteWithout | RewriteWith | Ignore.@marko/run-adapter*/*marko-run-adapter* → built-in Node. Packages: @marko/run-adapter-node (Express middleware: routerMiddleware(), matchMiddleware() → req.route.invoke), @marko/run-adapter-static (urls: [...] | async fn, crawls links), @marko/run-adapter-netlify ({ edge: true } + netlify.toml functions/edge_functions = "dist", publish dist/public).import * as router from "@marko/run/router" → router.fetch(request, platform), router.match(method, path) + router.invoke(route, request, platform).marko-run, run via bun run/bunx): dev (default, -p), build (-o), preview (-o -p -f); all take -c config, -e dotenv.GET before next() → whole response blocked. Pass the promise.next() from a successful POST → refresh resubmits, URL unshareable. Redirect instead.ctx.body() — it's a promise, not a function.Response never renders the page (its verb is dropped from the page's typed context).$$catch-all dir — never traversed.$global.session on the client — only params/url serialize by default; add to ctx.serializedGlobals (and never secrets).