Components and blocks

On this page

Components and blocks are small pieces of TSX that render to HTML at build time:

  • A component is used inside Markdown, e.g. :::callout{type=tip}.
  • A block is a section of a page, listed in the frontmatter blocks, e.g. a hero or a feature grid.

Both come from the theme or from the site. A site adds its own as files in components/ and blocks/, named after the file: components/badge.tsx is the badge component. A site file may not take the name of a theme one; to replace a theme's component or block, put a file of the same name in overrides/components/ or overrides/blocks/. Themes list theirs in defineTheme's components and blocks.

Components

// components/badge.tsx
import { defineComponent, z } from "@kilim-dev/core";

export default defineComponent({
  props: z.strictObject({
    tone: z.enum(["new", "beta"]).default("new"),
  }),
  examples: [":badge[Search]{tone=beta}", "Since 0.1 :badge[New]."],
  render({ tone }, { children }) {
    return <span class={`badge badge-${tone}`}>{children}</span>;
  },
});

Markdown uses a component in three ways:

SyntaxKindchildren
:::name{a=b} ... :::containerThe Markdown inside, rendered.
::name[label]{a=b}leafThe label, rendered.
:name[label]{a=b}textThe label, inside a sentence.

A container also gets its label, :::callout[Heads up], as label. Attributes are always strings, so a number prop needs z.coerce.number(). They are checked against props, and a mistake stops the build with the file and line:

content/guide.md:12: component "badge": tone: Invalid option: expected one of "new"|"beta"
content/guide.md:20: unknown component "bagde"; available: badge, callout.

An unknown container or leaf component is an error too. An unknown text directive stays plain text, since prose such as 10:30 or Note:this reads like one.

examples are Markdown snippets. kilim dev renders them in the catalog, and the admin's rich text editor inserts a component as a leaf (::name) when its examples only use it that way.

Blocks

// blocks/quotes.tsx
import { defineBlock, z } from "@kilim-dev/core";

export default defineBlock({
  props: z.strictObject({
    title: z.string().optional(),
    items: z
      .array(
        z.strictObject({
          // A text area in the admin, also while it is empty.
          text: z.string().meta({ multiline: true }),
          author: z.string(),
        }),
      )
      .min(1),
  }),
  examples: [
    {
      title: "What people say",
      items: [{ text: "Our pages got faster overnight.", author: "Ada" }],
    },
  ],
  render({ title, items }, { page, site }) {
    return (
      <section class="block quotes">
        {title && <h2>{title}</h2>}
        {items.map((item) => (
          <blockquote>
            <p>{item.text}</p>
            <footer>{item.author}</footer>
          </blockquote>
        ))}
      </section>
    );
  },
});

A page lists its blocks in the frontmatter, each with a type and its props:

---
title: Home
blocks:
  - type: quotes
    title: What people say
    items:
      - text: Our pages got faster overnight.
        author: Ada
  - type: content
---

This text goes where the `content` block is.

The collection schema needs a blocks field: blocks: blocksSchema.optional() with blocksSchema from @kilim-dev/core. It only checks that each item has a type; the block's own props check the rest, and an error names the file, line and block:

content/index.md:4: blocks[0].title: Invalid input: expected string, received number (block "quotes")

content is built in and places the page's Markdown body; a page with a body and no content block gets a warning. Besides its props, a block's render receives the page (with the rendered body as page.content) and the site, like a layout.

A string prop with .meta({ multiline: true }) gets a text area in the admin, for longer answers; .meta({ code: true }) gets one in a monospace font that keeps long lines unwrapped, for commands and code samples. A prop with a .default() shows its default while the key is absent, and choosing the default again leaves the key out of the file.

examples are props objects, without type. The catalog renders each one, and the admin's Add a block gallery inserts a block filled in with its first example, so give every block one that looks good. A later example whose choices (enum props such as layout: "bento" or tone: "contrast") differ from the first one's gets a tile of its own, named after them, like Features · Bento. The gallery shows the description of the props schema under the block's name: z.strictObject({ ... }).describe("Numbered steps, each with a command.").

The catalog

kilim dev serves /__kilim/blocks, a page that lists every component and block, where it comes from (the theme or a site file), a table of its props with their types, defaults and whether they are required, and its examples rendered with the theme's CSS. It is the quickest way to see what a theme offers, and is never part of a build.

A default export may also be a plain function (props, context) => ... without props checking or examples, for a quick component that takes no attributes.