Routing
There are two types of pages in almost any web project:
- unprocessed files (which are sent out unmodified to the browser), and
- pages that are generated by code (in what we call route handlers).
Unprocessed files
In Mastro, files placed in your project’s routes folder are sent out unmodified to the browser – with two exceptions:
*.client.tsfiles, which are type-stripped and served as*.client.js(see TypeScript), and*.server.jsor*.server.tsroute handlers, which generate pages (we’ll cover those below).
The simplest Mastro website is thus a single routes/index.html file, which will be sent out unprocessed. Other static assets can also be placed there (e.g. routes/favicon.ico).
For example, if you’d like to have a folder called assets, create it, and add a file routes/assets/styles.css, and perhaps one routes/assets/scripts.js. Then reference them like:
<!doctype html>
<html lang="en">
<head>
<title>My page</title>
<link rel="stylesheet" href="/assets/styles.css">
<script type="module" src="/assets/scripts.js"></script>
</head>
<body>
<h1>My page</h1>
</body>
</html>
See the Mastro guide for more about CSS, vanilla client-side JavaScript, and Reactive Mastro.
Links to other pages
To link to other pages, or to place images on your page, use the standard <a> and <img> elements respectively. Mastro does not use special <Link> or <Image> constructs.
For links (and also for references to unprocessed files), it’s easiest to always use absolute paths that start with a /. For example href="/assets/styles.css" above.
The file-based router (default)
While Mastro also comes with an Express-like programmatic router, the default router is file-based. This means the folder structure determines under what URL a route is served.
Just like unprocessed files, route handlers in Mastro are also placed in the routes folder. But if a file is named *.server.js (or *.server.ts), it’s a route handler (also known as page handler), and contains JavaScript functions to handle the generation of the pages at that route.
Route handlers
A route handler is a function that receives a standard Request object, and returns a standard Response object (or a Promise of such, meaning the function can be async).
Using the default file-based router, this function needs to be exported under the name of a HTTP method. For example for an HTTP GET:
export const GET = (req: Request) => {
return new Response("Hello World");
}
Since they return a standard Response object, route handlers can be used to generate HTML, JSON, XML, plain text (like the above example), binary data such as images, or whatever else you can think of. If you’re running a server, a Response can also represent a redirect.
Files and folders
Different hosting providers often serve the same file under slightly different urls. In Mastro, the URL for a file does not end with a slash, while the URL for a folder does end with a slash. Since a folder itself cannot contain any code, an index.html or index.server.js file is used to represent the containing folder.
File in routes/ |
URL |
|---|---|
file.html |
/file.html |
folder/index.html |
/folder/ |
file.server.js |
/file |
folder/index.server.js |
/folder/ |
folder/(folder).server.js |
/folder/ |
Since having lots of files called index.server.js would get confusing quickly, you can also name it (folder).server.js, where folder is the name of the containing folder.
Route parameters
Using route parameters, a single route can represent many different pages with the same basic url structure. For example, to match any URL of the form /blog/*/, you could use a route parameter called slug:
import { getParams } from "@mastrojs/mastro";
export const GET = (req: Request) => {
const { slug } = getParams(req);
return new Response(`Current URL path is /blog/${slug}`);
}
Above, slug is just an example name for the parameter. You can name parameters whatever you want, as long as it’s alphanumeric. (Mastro uses the standard URLPattern API under the hood.)
To capture URL segments containing slashes (often called rest parameters), use [...slug] instead of [slug].
Both of these can be used as the name of a folder inside routes/, or like above, as part of the route handler file name. To debug and console.log your routes, you can import { loadRoutes } from "@mastrojs/mastro".
When you’re using Mastro as a static site generator and have a route with a route parameter, Mastro cannot guess which URL paths you want to generate. In that case, you need to export an additional function getStaticPaths, which needs to return an array of strings (or a Promise of such):
export const getStaticPaths = async () => {
return ["/blog/my-first-post/", "/blog/we-are-back/"];
}
See the guide for an example of a static blog from markdown files.
Programmatic router
As an alternative to the file-based router, Mastro also offers a programmatic router, which is similar to Express.js or Hono. If you want to stick to the default router, jump to the next chapter now.
To try the programmatic router, modify the server.ts file in your project to something like the following.
import { getParams } from "@mastrojs/mastro";
import { Mastro } from "@mastrojs/mastro/server-programmatic";
const fetchHandler = new Mastro()
.get("/", () => new Response("Hello world")
.post("/", () => new Response("Hello HTTP POST")
.get("/blog/:slug/" => (req) => {
const { slug } = getParams(req);
return new Response(`Hello ${slug}`);
})
.createHandler();
Deno.serve(fetchHandler);
If you’re not using Deno, the last line will look different. But if you’ve started with a template using the file-based router, you basically replace mastro.fetch with the fetchHandler.
The first argument of each call (e.g. "/blog/:slug/") is used as the pathname to construct a standard URLPattern. The second argument is a route handler.
Organizing programmatic handlers
If you have more than a few routes in your programmatic router, it makes sense to place their handler functions in dedicated files (instead of using inline functions like above).
However, unlike with the file-based router, they don’t need to be in the routes folder. Since they’re normal JavaScript modules, you can name both the files and the functions whatever you want. However, it’s customary to create a handlers folder and follow one of the two naming conventions demonstrated in the following example.
If you want to export separate functions for GET and other HTTP verbs like POST, name the functions like that and later pass them to the router:
import { html, htmlToResponse } from "@mastrojs/mastro";
import { Layout } from "../components/Layout.ts";
export const GET = (req: Request) =>
htmlToResponse(
Layout({
title: "Home page",
children: html`<p>Welcome to ${req.url}</p>`,
}),
);
But usually it’s simpler to export a function called handler. In that case you can pass the whole module to the router:
import { html, htmlToResponse } from "@mastrojs/mastro";
import { Layout } from "../components/Layout.ts";
export const handler = (req: Request) =>
htmlToResponse(
Layout({
title: "About us",
children: html`<p>Welcome to ${req.url}</p>`,
}),
);
import { Mastro } from "@mastrojs/mastro/server-programmatic";
import * as Home from './handlers/Home.ts';
import * as About from './handlers/About.ts';
const fetchHandler = new Mastro()
.get("/", Home.GET)
.get("/about/", About)
.createHandler();
Deno.serve(fetchHandler);
More programmatic options
Using a module exporting a handler function, you can also export getStaticPaths and/or pregenerate variables. Those will be picked up by the router if you pass the whole module:
import * as News from './handlers/News.ts';
const fetchHandler = new Mastro()
.get("/news/:slug/", News)
.createHandler();
The above is equivalent to passing a config object manually:
.get("/news/:slug/", {
handler: (req) => new Response(`Hello ${req.url}`)
getStaticPaths: async () => (["/news/foo/", "/news/bar/"]),
pregenerate: true;
})
See also the API docs for the programmatic router.