Skip to content
MocklyDocs

Build

Collections

Blog posts, team members, and jobs as records with fields, and a page for each one.

A blog's posts, a team's members, open jobs, and FAQs are collections of records. Each record is a Markdown file with its fields at the top, and the studio shows them as a table and each one as a form. A page lists them, and a page per record shows each one.

Declare a collection#

A collection is declared once, in a component file, with the fields its records have:

components/blog.tsx
import { collection } from "mockly"; export const posts = collection(  "blog",  {    title: { type: "text", required: true },    published: { type: "date", required: true },    topic: { type: "choice", options: ["Product", "Statistics"] },    tags: "list",  },  { sort: "-published" },);

Its name, blog, is its folder: its records are the Markdown files in content/blog/.

Write a record#

Each record is a file, its fields between --- lines at the top, and its text under them:

content/blog/hello-world.md
---title: Hello, worldpublished: 2026-10-30topic: Producttags:  - news---Our first post, in **Markdown**.

The file's name is the record's slug, part of its page's address: lowercase letters, digits, and dashes.

Pages from records#

posts is a list of the records, newest first, as sort says. A page lists them like any list:

pages/blog/index.tsx
import { For, Page } from "mockly";import { posts } from "../../components/blog"; export default function Blog() {  return (    <Page title="Blog">      <ul>        <For each={posts}>          {(post) => (            <li>              <a href={`/blog/${post.slug}`}>{post.title}</a> · {post.published}            </li>          )}        </For>      </ul>    </Page>  );}

And a page with a [slug] makes a page per record, with <Markdown of={post} /> showing its text:

pages/blog/[slug].tsx
import { Markdown, Page } from "mockly";import { posts } from "../../components/blog"; export const paths = posts.map((post) => post.slug); export default function Post({ slug }: { slug: string }) {  const post = posts.find((p) => p.slug === slug);  return (    <Page title={post?.title ?? "Post"}>      <h1>{post?.title}</h1>      <p>{Math.max(1, Math.round((post?.words ?? 0) / 230))} min read</p>      <article class="prose">        <Markdown of={post} />      </article>    </Page>  );}

Each record has its fields, null for one that isn't required and isn't there, and its own slug, collection, and words, how many words its text has.

Fields#

TypeHoldsIn the studio
"text"A line of wordsA text field
"paragraph"Several linesA larger text field
"number"A numberA number field
"boolean"true or falseA switch
"date"A day, as 2026-10-30A date picker
"image"An image in public/, or an http(s) addressThe site's images, and Upload
"link"A page, such as /pricing, or an http(s) or mailto: addressA text field
"choice"One of its optionsA menu of them
"list"Words, such as tagsWords you add one at a time

A field written as its type, tags: "list", isn't required. Written out, it can be, and can have a label for the studio's form: { type: "text", required: true, label: "Headline" }.

sort puts records in order of a field, with - first for newest or largest first; records without it come last. Without sort, they're in order of slug.

In the studio#

Collections, in the editor's sidebar, lists each collection's records, newest first. New makes one from its title: its file is named after it, and today's date goes in the dates it needs. A record opens as a form of its fields, above its text, with the Markdown toolbar. Show shows its page in the preview. Its menu deletes it, asking first.

Content editors can make, write, and delete records too. A record reaches visitors when the site is published, as every file does, and History keeps each version.

Rules#

Records are checked against their collection's fields when the site is checked, each problem where it's written.

RuleCode
A collection is declared as the value of a top-level const, written out, with a name that's its folder, and fields of the types above.MK8004
A record has each field that's required, no field its collection doesn't, and each value of its field's type: a real day, one of a choice's options.MK8005
Fields at the top of a Markdown file are for a collection's records only.MK8002