FrontierJS

Mesa guide / Advanced

Markdown in depth

You need this page when a .md file does more than prose: it interpolates values, branches or loops, drops in interactive components, or runs through your own remark and rehype plugins. For the basics, see Markdown files.

---
title: Launch notes
tags: [mesa, release]
featured: true
---

<script>
  import Counter from './Counter.mesa'
  let likes = 0
</script>



{#if featured}
**This post is featured.**
{/if}

<Counter client:visible start={3} />

<p><button onclick={() => likes++}>Like {likes}</button></p>

Frontmatter keys become export const declarations, so {title} reads the frontmatter. The file extension decides the language: .md is compiled as markdown, and a --- block at the top of a .mesa file is only metadata.

{…} in prose is a path

In a .md file, a brace in prose interpolates a bare path (an identifier or a member chain) and nothing else. Anything else is literal text, with no error:

{title}                 interpolates
{post.author?.name}     interpolates
{ title }               literal: a path has no spaces
{items[0]}              literal: an index is not a member chain
{1, 2, 3}               literal
\{title}                renders {title}

That keeps a stray brace in prose from being compiled as JavaScript and eaten. The price is that {a + b} does not work in prose. Compute it in <script> (const sum = a + b) and write {sum}.

An attribute value is unchanged and takes any expression, as does every {…} in a .mesa file. An unclosed brace is still a compile error, in .md as in .mesa.

Block directives

{#if}, {#each} and the other blocks work in the markdown body:

{#if featured}
**This post is featured.**
{/if}

{#each tags as tag (tag)}
- {tag}
{/each}

Markdown is processed once per branch or iteration, so each pass of the {#each} above becomes its own <ul> with one item. For one list, write the HTML in the loop:

<ul>
  {#each tags as tag (tag)}
    <li>{tag}</li>
  {/each}
</ul>

Mesa closes no element for you, so an inline element on its own line is wrapped in a block yourself, as the <p><button> above is. Otherwise markdown opens a <p> that nothing closes.

client:*

A client:* directive on a component in markdown is an island, with the same meaning as in a .mesa file: client:load, client:idle, client:visible, client:media="(query)" and client:static. See Islands. Compile with islands: true to get the markers.

Compiling

import { compileSource, compileFile } from '@frontierjs/mesa'

const ctx = await compileSource(source, { filename: 'Post.md' })   // by extension
const same = await compileFile('./pages/about.md')                 // reads the file

ctx.result        // compiled JS module string
ctx.analysis      // .errors and .warnings
ctx.isStatic      // true if the page needs no JavaScript at runtime
ctx.frontmatter   // the parsed frontmatter, for .md and .mesa alike
ctx.layout        // frontmatter.layout, or null

compileSource routes on the filename: .md goes to the markdown compiler, anything else to the Mesa compiler. compile(source, config) always compiles Mesa, and compileMd from @frontierjs/mesa/compiler-md always compiles markdown.

Remark and rehype plugins

Pass your own plugins with two options. Remark plugins run after remark-gfm, and rehype plugins run after rehype-slug and before rehype-stringify. An entry is a plugin function or a [plugin, options] pair, as in unified's .use():

import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'

const ctx = await compileSource(source, {
  filename: 'Post.md',
  remarkPlugins: [remarkMath, [remarkSomething, { option: 'value' }]],
  rehypePlugins: [rehypeKatex],
})

The markdown packages (unified, remark, rehype) are optional peers, so the app installs them. With no plugins you pay nothing for the option.


Spec: VISION.md § 18.4 to 18.7 · RULE 63 · RULE 26