Marko in July 2026

TL;DR
  • Marko Run gained request validation, a data loading pattern, and typed URLs
  • Pages ship less JavaScript, and the compiler itself shrank by 22%
  • <style> tags accept ${...} interpolations for per-instance values
  • Compiler diagnostics suggest the correct form, and packages ship a cheat sheet for coding agents

July was the busiest month in the project's history by a wide margin. Marko Run picked up request validation, data loading, and typed URLs, a long optimization pass cut what pages ship to the browser, and <style> tags gained dynamic values.

Marko Run

Request validation, data loading, and URL construction became first-class in @marko/run, with type inference flowing from the handler through to pages and layouts (run#198).

In a +handler file, the verb helpers accept validation options for path parameters, the query string, and the request body. A validator is either a function or a Standard Schema, and validators are lazy, so a request that never reads ctx.search never pays to validate it.

export const GET = Run.GET(
  {
    params(value) {
      return { clientId: Number(value.clientId) };
    },
    search: InvoiceFilters,
  },
  (ctx, next) => next({ invoices: loadInvoices(ctx.params.clientId, ctx.search) }),
);

Handlers pass data downstream by calling next with an object, which templates read as $global.data. Passing the promise rather than awaiting it lets the response begin streaming while the data is still in flight, covered in Data Loading.

Run.href builds URLs against the routes the application actually serves, with the path, params, search, and hash all type checked. Renaming or moving a route turns stale links into compile-time errors instead of broken anchors, and client builds rewrite statically analyzable calls at compile time so most cost nothing at runtime (Typed URLs).

<await|invoices|=$global.data.invoices>
  <for|invoice| of=invoices>
    <a href=Run.href("/invoices/$clientId/$invoiceId", {
      params: {
        clientId: invoice.clientId,
        invoiceId: invoice.id
      }
    })>
      ${invoice.number}
    </a>
  </for>
</await>
<await|invoices|=$global.data.invoices>
  <for|invoice| of=invoices>
    <a href=Run.href("/invoices/$clientId/$invoiceId", {
      params: { clientId: invoice.clientId, invoiceId: invoice.id },
    })>
      ${invoice.number}
    </a>
  </for>
</await>
await|invoices|=$global.data.invoices
  for|invoice| of=invoices
    a href=Run.href("/invoices/$clientId/$invoiceId", {
      params: {
        clientId: invoice.clientId,
        invoiceId: invoice.id
      }
    }) --
      ${invoice.number}
await|invoices|=$global.data.invoices
  for|invoice| of=invoices
    a href=Run.href("/invoices/$clientId/$invoiceId", {
      params: { clientId: invoice.clientId, invoiceId: invoice.id },
    }) --
      ${invoice.number}

A POST handler calling next now renders the page instead of falling through to the GET handler. The documentation grew alongside the feature, with new guides covering routing, validation, data loading, and the runtime (website#183).

Performance

The optimization work this month split between the code pages ship and the compiler developers install.

Pages stopped carrying runtime they cannot use. A spread onto any native tag used to pull in every controlled-element handler, so a page that only spread onto a <div> still shipped the machinery for <select>, <input>, and <textarea>. Such a page now ships roughly 37% less JavaScript, dropping from 2,614 to 1,641 bytes after brotli, and one spreading onto a <textarea> drops from 1,981 to 1,529. Pages that genuinely use every control kind are unchanged (marko#3622).

Selecting a row in a keyed <for> became an O(1) update. When a value outside the loop is read inside it only as an equality against the loop's key, a change can flip at most two rows, so the compiler updates just those two instead of walking the list. The update measures flat from 1,000 to 20,000 rows, and loops that do not match the pattern are unaffected (marko#3314).

Server rendering got faster and its output got smaller. Rendering a page spends less time producing the same bytes (marko#3411), and every <if> and <for> branch on the page now sends less data to the browser to resume from (marko#3320). Serving those renders got faster too: a Marko Run application on Node measured about 6% higher requests per second on a 16 KB page and about 3% on a 455-byte page in a local benchmark (run#214).

The compiler itself got smaller as well, dropping from 2,084 KB to 1,626 KB on Node (about 22%) and from 1,966 KB to 1,509 KB in the browser (about 23%). It compiles to byte-identical output, so the saving is purely in what gets installed and loaded (marko#3688).

Application-level techniques are collected in Optimizing Performance.

Dynamic Styles

The <style> core tag accepts ${...} interpolations. The stylesheet itself stays static and is still extracted into the bundle, with the changing values delivered through CSS custom properties. Every instance gets its own values, they update independently in the browser, and they work under a CSP nonce.

<style>
  
  .status-pill {
    background: ${input.tint};
    padding-inline: calc(${input.pad} * 1px);
  }

</style>
<span class="status-pill">${input.label}</span>
<style>
  .status-pill {
    background: ${input.tint};
    padding-inline: calc(${input.pad} * 1px);
  }
</style>

<span class="status-pill">${input.label}</span>
style
  --


    .status-pill {
      background: ${input.tint};
      padding-inline: calc(${input.pad} * 1px);
    }


  --
span class="status-pill" -- ${input.label}
style
  --

    .status-pill {
      background: ${input.tint};
      padding-inline: calc(${input.pad} * 1px);
    }

  --

span class="status-pill" -- ${input.label}

Interpolated values are escaped, so user input cannot break out of the stylesheet. Interpolations belong in property values rather than in selectors, property names, or quoted strings, and a unit belongs inside the value or a calc() rather than written against the interpolation. A dynamic <style> applies to the elements after it, so it goes above the content it styles (marko#3309).

Agent Guidance

A run of work aimed the compiler's diagnostics and the published packages at coding agents as well as the developers supervising them.

The compiler's diagnostics now suggest the Marko form to write when a template reaches for a habit carried over from another dialect, whether in control flow, tag variables, or attribute values. Each suggestion states the correct general form only, never task-specific content (marko#3388). A template's errors are also reported together rather than one at a time, so a single compile surfaces everything to fix (marko#3390).

The marko package now ships a dense syntax reference at cheatsheet.md, covering the Tags API rules, control flow, async rendering, components, and client effects. Publishing it inside the package gives tooling a stable path to point agents at, and @marko/vite and @marko/run ship the same reference and route agents to it (marko#3392, vite#283, run#212).

Improvements

The rest of the month's feature and tooling work centered on pnpm support, serialization, and the playground.

pnpm

Using a published tag library from a pnpm project did not reliably work. Tag libraries are discovered through a path that resolves symlinks, and under pnpm's virtual store that path is not importable, so a tag from an installed package compiled to an import pointing nowhere. Tag definitions now carry the name they were resolved by and import through it, which also keeps aliased dependencies working (marko#3418, marko#3501, marko#3610). The language server resolves those tags the same way, so editors follow them too (language-server#559).

Builds were affected as well. Template discovery in @marko/vite walked node_modules following symlinks, and since pnpm links packages into a cyclic graph the walk never terminated, so an optimized build exhausted the heap rather than finishing. It now completes in about 2.6 seconds (vite#297). pnpm create marko scaffolds cleanly too, since pnpm blocks package build scripts by default and esbuild never finished installing in Vite-based templates (cli#235). Marko's own repositories moved to pnpm over the month, which is how most of this surfaced (marko#3514 among others).

Serialization

Values that previously aborted a server render now survive it. Holding a formatter in a <const/fmt=new Intl.NumberFormat(...)> and using it from content that updates in the browser used to crash the render, and now works (marko#3566). All eight Temporal types come back exactly as they went in, keeping calendar, time zone, and nanosecond precision (marko#3568), and DataView joined the typed arrays already supported (marko#3571). None of this adds anything to the browser bundle.

Playground

Compile errors in the playground now render as a real code frame. They previously came out as plain text, so the carets no longer lined up under the code they pointed at. An error now shows highlighted source with its carets in the right place, and long lines scroll to keep the caret in view (website#200).

Tag libraries can be declared in a package.json tab and their tags used with no imports, with dependencies installed from npm in the browser (website#184). Package installs got faster with a loading indicator (website#188), and Prettier now loads on first format rather than on page load (website#222).

Scaffolding

The marko-js/cli repository was trimmed down to the project scaffolding tool and rebuilt, with @marko/create rewritten in TypeScript. The CLI flags are unchanged. Scaffolding now works unattended, which matters for CI and for agents: --yes and automatic CI detection fall back to defaults or fail with a clear error instead of hanging on a prompt (cli#231, cli#241).

Examples

A new app example pairs an @marko/run skeleton with tooling already wired up, including Vitest with server and browser projects, Storybook, ESLint, Prettier, and type checking. It is the counterpart to basic, for starting a real application rather than a demo (examples#57).

Lifecycle

The <lifecycle> tag can initialize its state by returning an object from onMount, so an instance created there is available to onUpdate and onDestroy without a separate assignment (marko#2957).

Fixes

Correctness work concentrated on controlled elements, resuming, and asynchronous rendering. Server and browser rendering came into agreement across <select>, <textarea>, and radio groups, covering form reset, value coercion, and multiple selections in any order, so a controllable element ends up in the same state either way. Roughly two dozen resume fixes followed, including one where a single unserializable value could take down a page's whole resume rather than just reporting itself. Placeholder and error-boundary behavior improved across <await> and <try>, lazy loading triggers and fallbacks were hardened, and module resolution no longer loops forever at a Windows drive root.

Full details for every change are in the release notes of each package on GitHub.

Community

Livestream

Anthony Campolo hosted Marko 6 with Dylan Piercey, a livestream on why Marko 6 exists, where it shines, and what is coming next.

Knip

Knip ships a Marko plugin, built by @caseycarroll (knip#1914). It enables itself when marko is a dependency, reads marko.json and marko-tag.json, and treats .marko component and tag files as entry points, so unused files and exports are reported accurately in a Marko project. @webpro announced it.

Interactive Tutorial

@defunkt-dev built an interactive Marko tutorial that runs Node and the compiler in the browser through WebContainers. Lessons sit beside a live editor and preview, so each step is edited and run in place, and the material teaches Marko 6.

TanStack Virtual

The same author wrote @tanstack/marko-virtual, a Marko 6 adapter for TanStack Virtual. It covers row, column, and grid virtualization through two auto-discovered tags, <virtualizer> for element scrolling and <window-virtualizer> for full-page scrolling.

Further Reading


Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.