Guides

Writing pages in Markdown

A guide to publishing Markdown content as Effront pages.

Publish Markdown content as pages in your Effront application, with article links and local assets resolved to their public URLs. The example renders an article with @effront/markdown and its configured Comark-based React renderer.

Add an article

Install the collection/parser and React renderer in your application:

vp add @effront/markdown@0.2.0

Create src/content/intro.md:

# Introduction

Welcome to the manual.

Keep collection imports and parsing in server-side modules.

Load the collection

Create src/manual.ts:

import { createMarkdownCollection } from "@effront/markdown";

export const manual = createMarkdownCollection({
  basePath: "/manual",
  documents: import.meta.glob<string>("./**/*.md", {
    base: "./content",
    query: "?raw",
    import: "default",
    eager: true,
  }),
});

This maps intro.md to /manual/intro for lookup, but does not register an application route.

If articles use local images or downloads, update src/manual.ts to define and pass assets:

// src/manual.ts: add before manual.
const assets = import.meta.glob<string>("./**/*.{svg,png,jpg,pdf}", {
  base: "./content",
  query: "?url",
  import: "default",
  eager: true,
});

// Replace the manual declaration, adding assets.
export const manual = createMarkdownCollection({
  basePath: "/manual",
  assets,
  documents: import.meta.glob<string>("./**/*.md", {
    base: "./content",
    query: "?raw",
    import: "default",
    eager: true,
  }),
});

Include every referenced asset's file type in the glob. Use the same base for documents and assets so relative references match.

Render the article at its URL

In src/entry.effront.tsx from Getting started, add the imports, define IntroPage, and register it alongside the homepage:

// src/entry.effront.tsx: add to the imports.
import { MarkdownDocument } from "@effront/markdown/document";
import "@effront/markdown/styles.css";
import { parseMarkdown } from "@effront/markdown";
import { manual } from "./manual";

// Add before the default export.
const IntroPage = EFFRONT.Page.make({
  render: Effect.fn("IntroPage.render")(function* () {
    const collection = yield* manual;
    const entry = collection.get("/manual/intro");
    if (!entry) throw new TypeError("Registered article is missing");
    const document = yield* parseMarkdown(entry);
    return (
      <article>
        <MarkdownDocument value={document} />
      </article>
    );
  }),
});

// Replace the default export.
export default EFFRONT.make({
  routes: EFFRONT.Routes.make({ layout: RootLayout })
    .page("/", HomePage)
    .page("/manual/intro", IntroPage),
});

Open /manual/intro to see the article inside your Layout. Markdown body styling is not provided. Style it in your application or use a library such as Tailwind Typography; see Styling.

When src/content/details.md has a Page registered at /manual/details, [Details](./details.md#example) in intro.md resolves to /manual/details#example. Relative asset references resolve from the article's directory to their imported URLs. Missing references fail with MarkdownError.

For a catch-all route, follow the complete collection example. Look up the requested article in HTTP middleware and return 404 before rendering starts when get() returns undefined. The fixed-route example above instead treats a missing registered article as a configuration error. Collection and parsing failures also use the MarkdownError Effect error channel.

Customize parsing or rendering

Use Comark syntax with parseMarkdown(entry) and Effront's defaults. To change parsing, pass Comark ParserOptions as the second argument, for example parseMarkdown(entry, { linkify: false }).

To add a parser plugin, install comark@0.6.2 as a direct dependency and update src/entry.effront.tsx:

// src/entry.effront.tsx: add to the imports.
import toc from "comark/plugins/toc";

// Replace IntroPage, keeping its route registration unchanged.
const IntroPage = EFFRONT.Page.make({
  render: Effect.fn("IntroPage.render")(function* () {
    const collection = yield* manual;
    const entry = collection.get("/manual/intro");
    if (!entry) throw new TypeError("Registered article is missing");
    // Add the parser plugin to this call.
    const document = yield* parseMarkdown(entry, { plugins: [toc()] });
    return (
      <article>
        <MarkdownDocument value={document} />
      </article>
    );
  }),
});

Additional plugins run after Effront's defaults, not instead of them. The MarkdownDocument and stylesheet imports above are sufficient to render the parsed document. Math and Mermaid are registered by default, with no individual registration needed. To customize them, wrap the exported Math from @effront/markdown/math or Mermaid from @effront/markdown/mermaid with your desired options, or implement your own components. Pass your replacements through MarkdownDocument's components. See Comark's React renderer for component options.

See the Markdown reference for options and reference-resolution rules.