Mastro šŸ‘Øā€šŸ³ Docs

Search results

Blog Community GitHub   Stoat Chat   Bluesky

Components and HTML

To construct HTML, Mastro exports the html tagged template literal, which makes sure things are properly escaped (unless unsafeInnerHtml is used).

For syntax-highlighting, be sure to configure your editor accordingly. For example for VSā€ŠCode, we recommend the FAST Tagged Template Literals extension.

Using template literals means that you have complete control over your HTML. You’re free to write HTML instead of XHTML. And while it says html on the tin, you can also use this to construct SVG or XML strings – just make sure you run it through a validator.

import { html, renderToString } from "@mastrojs/mastro";

const myName = "World";
const btnClass = "btn";

const str = await renderToString(
  html`
    <h1>Hello ${myName}</h1>
    <a href="/" class=${btnClass}>Home</a>
  `
);
Copied!

Unlike JSX or Astro templates, you don’t need to set up a build step. You can easily use the plain HTML string wherever you want (e.g. in a one-off script, or for sending HTML emails via an API).

However, usually you will directly construct a standard Response object using htmlToResponse:

routes/index.server.ts
import { html, htmlToResponse } from "@mastrojs/mastro";
import { Layout } from "../components/Layout.ts";

export const GET = (req: Request) =>
  htmlToResponse(
    Layout({
      title: "Hello world",
      children: html`<p>Welcome!</p>`,
    })
  );
Copied!

Layout is a component, which may be implemented as follows.

Components

A Mastro server-side component is just a normal JavaScript function. By convention, the function name is usually capitalized, takes a props object, and returns something of type Html. There’s really no magic going on here.

For various ways to structure your CSS, see component-scoped CSS with Mastro.

Let’s look at how the Layout component from above might be defined. Notice that it is in turn calling a component called Header.

components/Layout.ts
import { html, type Html } from "@mastrojs/mastro";
import { Header } from "./Header.ts";

interface Props {
  title: string;
  children: Html;
}

export const Layout = (props: Props) =>
  html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <title>${props.title}</title>
      </head>
      <body>
        ${Header()}
        <h1>${props.title}</h1>
        ${props.children}
      </body>
    </html>
  `;
Copied!

HTML Streaming

Promises and AsyncIterables can be passed directly into HTML templates without needing to be awaited.

For static site generation, this doesn’t matter (in fact, awaiting will likely result in a slightly faster generation step). But when running a server, you should pass in async values without awaiting them. htmlToResponse will then create a Response that sends the chunks over the wire as soon as they’re available.

This can dramatically speed up time to first byte: a user can start reading the top of your page, while the last row hasn’t even left the database yet. In HTTP/1.1, this was known as chunked transfer encoding, but in HTTP/2 and HTTP/3 it’s built in at the lower levels of the protocol.

To not break streaming, make sure you:

routes/index.server.ts
import { html, htmlToResponse } from "@mastrojs/mastro";
import { Layout } from "../components/Layout.ts";
import * as db from "../database.ts";

export const GET = (req: Request) => {
  // no await here to not break streaming
  const titlePromise = db.loadTitle();
  const rows = db.loadWidgets();

  return htmlToResponse(
    Layout({
      title: "My widgets",
      children: html`
        <h1>${titlePromise}</h1>
        <ul>
          ${mapIterable(rows, (row) =>
            html`<li>${row.title}</li>`
          )}
        </ul>
      `,
    })
  );
}

/**
 * Maps over an `AsyncIterable`, just like you'd map over an array.
 */
async function * mapIterable<T, R> (
  iter: AsyncIterable<T>,
  callback: (val: T) => R,
): AsyncIterable<R> {
  for await (const val of iter) {
    yield callback(val)
  }
}
Copied!

We use our own mapIterable function, because while standard Iterator helpers are specced and implemented, async iterator helpers are still a work in progress. See this working example

To stream JSON, see HTTP Streaming in the guide.

Reading files

To abstract over the different environments Mastro runs in – the Mastro VSā€ŠCode extension, Deno, Node.js and Bun – the @mastrojs/mastro package also exports a few functions to read out files from your project folder: readDir, findFiles (uses fs.glob), readTextFile, and readFile.