Build
Pages and routes
Files in pages/ become routes. Each page renders a <Page> with its title and metadata.
Each file in pages/ is a page of your site, and its path decides the page's URL. A page file exports one component, which returns a <Page>: the page's title and the other tags for its <head>, around the markup of its body.
import { Page } from "mockly"; export default function Pricing() { return ( <Page title="Pricing · Acme" description="Plans for every team."> <main class="mx-auto max-w-3xl px-6 py-20"> <h1 class="text-4xl font-bold">Pricing</h1> <p class="mt-4 text-gray-600">Start free, and upgrade when you grow.</p> <a href="/" class="mt-8 inline-block underline"> Back home </a> </main> </Page> );}This file is published at /pricing, as HTML that's ready before anyone visits.
Files and routes#
| File | Route |
|---|---|
pages/index.tsx | / |
pages/pricing.tsx | /pricing |
pages/docs/index.tsx | /docs |
pages/docs/setup.tsx | /docs/setup |
pages/posts/[slug].tsx | /posts/hello, /posts/launch, and so on: one page per entry of its paths |
pages/404.tsx | The not-found page |
File and folder names use lowercase letters, digits, and dashes, and files end in .tsx. An index.tsx file is its folder's route. A name in square brackets is a parameter (see Pages from data). Any other name is MK1015.
Page routes never contain a dot. That's how Mockly tells pages from files: /pricing is a page, and /guide.pdf is a file from public/ (see Images, fonts, and files). A URL with a trailing slash, such as /pricing/, redirects to the same URL without it.
What a page file contains#
A page file has one default export, the page component, whose root element is <Page> (MK2008). It takes no props, unless its path has parameters. Next to it, the file can hold top-level constants and components that only this page uses.
Apart from paths on a page with parameters, a page exports nothing else (MK1002), and other files can't import it (MK5007). Shared components and data go in components/; see Components and styling.
<Page> can only be the root of a page component (MK2010), so a layout component can't render it. Put the parts that pages share, such as a header and a footer, in components that each page renders inside its <Page>.
The not-found page#
pages/404.tsx is optional. When it exists, the site sends it, with status 404, for every page URL that matches no page. Without it, visitors get a plain default page.
import { Page } from "mockly"; export default function NotFound() { return ( <Page title="Page not found · Acme"> <main class="mx-auto max-w-xl px-6 py-32 text-center"> <h1 class="text-4xl font-bold">This page isn't here</h1> <a href="/" class="mt-8 inline-block underline"> Back home </a> </main> </Page> );}The <Page> element#
<Page> writes the whole document around your markup: the doctype, <html>, <head>, and <body>. Its props become the tags in <head>.
| Prop | Type | Description |
|---|---|---|
title | string | Required. The page's <title> and its og:title tag. |
description | string | The meta description and og:description. |
lang | string | The page's language, on <html lang>. Defaults to "en". |
icon | URL | The favicon, such as "/favicon.svg". Its type comes from the file's extension. |
image | URL | The image shown when the page is shared, ideally 1200×630. It's made absolute against the site's URL, and the page gets a large Twitter card. |
imageAlt | string | A description of image, as og:image:alt. |
noindex | boolean | Keeps the page out of search results and out of the sitemap. |
themeColor | string | A color for the browser's interface, as <meta name="theme-color">. |
fonts | Font[] | Font files the page uses. Each one gets a preload, an @font-face rule, and a font-<name> class. See Images, fonts, and files. |
jsonLd | object or list of objects | Structured data for search engines, written as a <script type="application/ld+json">. |
stylesheets | list of paths | CSS files in public/ the page links, such as ["/css/site.css"], after the site's own stylesheet. See Images, fonts, and files. |
utilities | boolean | false leaves out Mockly's utility classes and their reset on this page, so its own stylesheets style it over the browser's defaults. Imported pages set it. See Importing a site. |
htmlAttributes | object | Attributes for <html>, such as { class: "no-js", dir: "rtl" }. Set the language with lang. |
bodyAttributes | object | Attributes for <body>, such as { class: "home" }. |
goal | string literal | A goal recorded each time a visitor is served the page, such as a thank-you page's "signup". See Goals. |
heatmap | true or false | Whether the page records a heatmap; without it, the page follows the site's setting. See Heatmaps. |
Every page also gets og:type, og:site_name with the site's name, and twitter:card. When the site's URL is known, each page gets og:url and a canonical link. A noindex page gets no canonical link.
A page that someone reaches only after signing up can record the goal and stay out of search results:
import { Page } from "mockly"; export default function Thanks() { return ( <Page title="Thanks · Acme" goal="signup" noindex> <main class="mx-auto max-w-xl px-6 py-32 text-center"> <h1 class="text-3xl font-bold">You're on the list</h1> </main> </Page> );}Values and checks#
Prop values can be expressions, such as a constant or a template literal. The type checker checks each prop's type as you write. Values are checked again when the page renders at publish time: a value that can't be used, such as a javascript: URL in icon or a font file that isn't .woff2, .woff, .ttf, or .otf, stops the publish with MK1014, naming the prop.
A missing title is MK2013, and a goal that isn't a string literal is MK4016.
Sharing props between pages#
Spread attributes aren't supported (MK2005), so pass each prop. To keep pages consistent, put the shared values in a constant in components/ and read them on each page:
// components/site.tsxexport const head = { icon: "/favicon.svg", image: "/og.png", imageAlt: "Acme: launch pages that learn", themeColor: "#4f46e5",};<Page title="Changelog · Acme" icon={head.icon} image={head.image} imageAlt={head.imageAlt} themeColor={head.themeColor}>Pages from data#
A file or folder named in square brackets, such as [slug], is a parameter. The page exports paths, a top-level constant that lists its pages, and Mockly publishes the page once for each entry, with the entry's values as props:
import { Page } from "mockly"; const posts = [ { slug: "hello", title: "Hello, world", body: "Our first post." }, { slug: "launch", title: "We launched", body: "Acme is live." },]; export const paths = posts.map((p) => p.slug); export default function Post({ slug }: { slug: string }) { const post = posts.find((p) => p.slug === slug); return ( <Page title={`${post?.title ?? "Post"} · Acme`}> <article class="mx-auto max-w-2xl px-6 py-20"> <h1 class="text-4xl font-bold">{post?.title}</h1> <p class="mt-6 text-lg text-gray-700">{post?.body}</p> </article> </Page> );}This publishes /posts/hello and /posts/launch.
- Parameters have camelCase names, such as
[slug]or[postId], and each name appears once per path. A path can have several:pages/blog/[year]/[slug].tsx. - Entries. With one parameter, each entry of
pathsis a string or a whole number. With several, each entry is an object with a string for each parameter:[{ year: "2026", slug: "hello" }]. - Props are the parameters, by name. A prop that isn't one is MK1016, as is a page with parameters and no
paths, orpathson a page without parameters. - Values are 1 to 100 letters, digits,
-,_, or~. A value that isn't, an entry listed twice, a page another page already is, or more than 1,000 entries stops the publish with MK1017. - Each page is a full page, with its own HTML, title, canonical link, and place in the sitemap. A URL that isn't one of the entries gets the not-found page.
- Clashes. Two routes that match the same URLs, such as
pages/posts/[slug].tsxandpages/posts/[id].tsx, are MK1015.
Most often, paths comes from data fetched when you publish, such as a CMS's list of posts; see Data from APIs. The list of pages is fixed when you publish, so a new entry appears with the next publish. In the studio, the preview shows a route's first page, with a picker for the others.
Links between pages#
Link to a page by its path from the site's root:
<a href="/pricing">Pricing</a><a href="/#faq">Questions</a>Root-relative links work wherever the site is served. When the studio serves a site at its address, such as /s/acme/launch for the site launch in the organization acme, every root-relative URL gets that prefix for you.
The compiler checks every link written as a string. A root-relative link that matches no page is a warning, MK2015, and its hint lists the site's pages. The query, the fragment, and a trailing slash are ignored, and /posts/anything matches a route with parameters. Links built from data, such as `/posts/${post.slug}`, aren't checked. A link whose last segment has a dot, such as /guide.pdf, points at a file, and is MK2014 when no file in public/ matches it.
Links to other sites can use http, https, mailto, and tel. A link written with any other scheme, such as javascript:, is MK2007.
When you rename a page in the studio, links to it written as strings change with it. Deleting a page lists the files that still link to it.
Search engines#
Each publish writes two files at the site's root:
sitemap.xmllists every page that isn'tnoindex, by its full URL, including every page made from data.robots.txtlets every crawler in and names the sitemap.
Turning off indexing in the studio's site settings gives every page noindex, and makes robots.txt turn every crawler away.
The full URLs in the sitemap, canonical links, and share images come from the site's URL. In the studio, set it in Site settings; until you do, publishes use the studio's own address for the site. With the CLI, pass --site-url; a build without it has no sitemap.
To describe a page to search engines in more detail, give jsonLd an object from schema.org, or a list of them:
<Page title="Acme" jsonLd={{ "@context": "https://schema.org", "@type": "Organization", name: "Acme", url: "https://acme.example", }}>