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 a template literals means that you have complete control over your HTML. You’re free to write HTML instead of XHTML. That being said, while it says html on the tin, you can actually also use this to construct SVG or XML strings. It’s just not going to be enforced that it’s valid XML unless you run it through a validator (which you should be doing anyway).
import { html, renderToString } from "@mastrojs/mastro";
const myName = "World";
const btnClass = "btn";
const str = renderToString(
html`
<h1>Hello ${myName}</h1>
<a href="/" class=${btnClass}>Home</a>
`
);
However, usually you will directly construct a Response object using htmlToResponse:
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>`,
})
);
In the above example, Layout() is a component call.
Components
A Mastro server-side component is just a normal JavaScript function, that by convention is capitalized, takes a props object, and returns something of type Html. There’s really no magic going on here.
Let’s look at how a Layout component might be defined. Notice that it is in turn calling a component called Header.
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>
`;
HTTP Streaming
Promises and AsyncIterables can always be passed directly into HTML templates without needing to be awaited – even when using htmlToResponse.
However, to construct a Response that sends the chunks over the wire as soon as they’re available, use htmlToStreamingResponse. When running a server, 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. This used to be known in HTTP/1.1 as chunked transfer encoding, but is built into HTTP/2 and HTTP/3 at the protocol level.
import { html, htmlToStreamingResponse } from "@mastrojs/mastro";
import { Layout } from "../components/Layout.ts";
import * as db from "../database.ts";
export const GET = async (req: Request) => {
const rows = db.loadWidgets();
return htmlToStreamingResponse(
Layout({
title: "My widgets",
children: html`
<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)
}
}
(While standard Iterator helpers are specced and implemented, Async Iterator Helpers are still a work in progress.)
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.