Process Markdown and Markdoc files into Svelte components using Markdoc. Use Markdoc defaults out of the box or configure Markdoc schema to your needs.
Install markdoc-svelte
in your SvelteKit project.
npm install markdoc-svelte
Amend your SvelteKit config in svelte.config.js
to:
- Process files with the extensions you choose (such as
.mdoc
and.md
). - Include the preprocessor.
import { markdocPreprocess } from "markdoc-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: [".svelte", ".mdoc", ".md"],
preprocess: [markdocPreprocess()],
};
Create a directory to hold your markdoc files: src/lib/markdoc
In that directory (at src/lib/markdown/content.md
), add an example with basic Markdown syntax:
---
title: Hello World
---
# Hello World
This is a file that is processed by `markdoc-svelte`.

Dynamically import all files in the directory using a catchall route at src/routes/[...catchall]/+page.ts
:
import { error } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
import type { MarkdocModule } from "markdoc-svelte";
export const load: PageLoad = async ({ params }) => {
const slug = params.catchall;
try {
const page = (await import(`$lib/markdown/${slug}.md`)) as MarkdocModule;
return { page };
} catch {
throw error(404, `No corresponding file found for the slug "${slug}"`);
}
};
Render the imported file as a Svelte component in a file at src/routes/[...catchall]/+page.svelte
:
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>{data.page.frontmatter?.title ?? 'Undefined title'}</title>
</svelte:head>
<data.page.default />
Run your dev server and visit /content
to see the rendered file.
Optionally define YAML frontmatter in your file.
Then use it in your content with the $frontmatter
variable.
---
title: Why I switched to Markdoc
description: What the benefits of Markdoc are and how to take advantage of them.
---
# {% $frontmatter.title %}
You can also access the frontmatter in your Svelte page components.
Get it from the data you defined in +page.ts
:
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>{data.page.frontmatter?.title ?? 'Default title'}</title>
<meta name="description" content={data.page.frontmatter?.description ?? 'Default description'} />
</svelte:head>
<data.page.default />
To add additional features to the syntax of your files, customize your Markdoc schema. You can add the following extensions:
You can customize schema in two ways:
- For a single extension or simple extensions, pass directly to the preprocessor options.
- For multiple extensions at once or more complex configurations, create a configuration folder with schema definitions.
For each extension (such as nodes
),
schema definitions passed directly overwrite configuration from a folder.
To define any of the extension points directly,
pass it to the preprocessor options as the name of the extension.
For example, to define a $site.name
variable, pass the following:
import { markdocPreprocess } from "markdoc-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: [".svelte", ".mdoc"],
preprocess: [
markdocPreprocess({
variables: {
site: {
name: "Markdoc Svelte",
},
},
}),
],
};
For multiple extensions or more complex configurations, create a folder with your entire Markdoc schema.
By default, the preprocessor looks for a schema in the ./markdoc
and ./src/markdoc
directories.
Define each extension point as a single file
or as a directory with an index.ts
or index.js
file that exports it.
Partials must be a directory holding Markdoc files.
All extension points are optional.
Example structure:
markdoc
├── functions.ts
├── nodes
│ ├── heading.ts
│ ├── index.ts
│ └── callout.ts
├── partials
│ ├── content.mdoc
│ └── more-content.mdoc
├── tags.ts
└── variables.ts
For example, create custom nodes in markdoc/nodes.ts
:
import type { Config } from "markdoc-svelte";
import { Markdoc } from "markdoc-svelte";
const nodes: Config["nodes"] = {
image: {
render: "EnhancedImage",
attributes: {
...Markdoc.nodes.image.attributes, // Include the default image attributes
},
},
};
export default nodes;
Or create an index file to export all custom nodes from markdoc/nodes/index.ts
(remember to use relative imports):
import image from "./image.ts";
import link from "./link.ts";
import paragraph from "./paragraph.ts";
import type { Config } from "markdoc-svelte";
const nodes: Config["nodes"] = {
image,
link,
paragraph,
};
export default nodes;
You can use relative imports to import definitions from either .js
or .ts
files.
Just remember to include the file extension.
For example, if you define custom functions in src/lib/functions.js
,
add them to your schema as follows:
import { markdocPreprocess } from "markdoc-svelte";
import functions from "./src/lib/functions.js";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [
markdocPreprocess({
functions: functions,
}),
],
};
Option | Type | Default | Description |
---|---|---|---|
comments |
boolean | true |
Enable Markdown comments |
components |
string | "$lib/components" |
Svelte components directory for custom nodes and tags |
extensions |
string[] | [".mdoc", ".md"] |
Files to process with Markdoc |
functions |
Config['functions'] | - | Functions config |
layout |
string | - | Default layout for all processed Markdown files |
linkify |
boolean | false |
Auto-convert bare URLs to links |
nodes |
Config['nodes'] | - | Nodes config |
partials |
string | - | Partials directory path |
schema |
string | ["./markdoc", "./src/markdoc"] |
Schema directory path |
tags |
Config['tags'] | - | Tags config |
typographer |
boolean | false |
Enable typography replacements |
validationLevel |
ValidationLevel | "error" |
Validation strictness level |
variables |
Config['variables'] | - | Variables config |
Functions enable you to add custom utilities to Markdown so you can transform content and variables.
For example, you could add a function for transforming strings to uppercase to markdoc/functions.ts
:
import type { Config } from "markdoc-svelte";
const functions: Config["functions"] = {
uppercase: {
transform(parameters) {
const string = parameters[0];
return typeof string === "string" ? string.toUpperCase() : string;
},
},
};
export default functions;
Then you can use the custom function in a Markdown file:
---
title: Hello World
---
This is a {% uppercase(markdown) %} file that is processed by `markdoc-svelte`.
Nodes are elements built into Markdown from the CommonMark specification. Customizing nodes enables you to change how existing elements from Markdown are rendered using Svelte components. The components are automatically loaded from the components directory defined in your configuration.
For example, you might want to customize how images are displayed using the @sveltejs/enhanced-img
plugin.
First, define a custom node in markdoc/nodes.ts
:
import type { Config } from "markdoc-svelte";
import { markdocPreprocess } from "markdoc-svelte";
const nodes: Config["nodes"] = {
image: {
render: "EnhancedImage",
attributes: {
// Include the default image attributes
...Markdoc.nodes.image.attributes,
},
},
};
Then add an EnhancedImage component in src/lib/components/EnhancedImage.svelte
:
<script lang="ts">
// Glob import all Markdown images
const imageModules = import.meta.glob(
'$lib/images/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp,svg}',
{
eager: true,
query: {
enhanced: true,
},
}
) as Record<string, { default: string }>;
const { src, alt, ...restProps } = $props();
// Find the image module that matches the src
const matchingPath = Object.keys(imageModules).find((path) => path.endsWith(src));
const image = matchingPath ? imageModules[matchingPath].default : undefined;
</script>
{#if image}
<!-- Render the image with the enhanced-img plugin -->
<enhanced:img src={image} {alt} {...restProps} />
{:else}
<img src={src} {alt} {...restProps} />
{/if}
Now your EnhancedImage component handles images added through standard Markdown syntax:

Partials are ways to reuse content across files (through transclusion). The partials defined in your configuration must be a directory of Markdoc files.
For example, you could have a file structure like the following:
| markdoc/
|-- partials/
| |-- content.mdoc
| └── post.mdoc
These files can be included in other files as follows:
---
title: Hello World
---
# Hello World
This is a file that is processed by `markdoc-svelte`.
{% partial file="content.mdoc" %}
{% partial file="post.mdoc" %}
Tags are ways to extend Markdown syntax to do more. You can add functionality through Svelte components
For example, you might want to create a custom Callout tag to highlight information on a page
(these are also known as admonitions).
First, define the tag in markdoc/tags.ts
:
import type { Config } from "markdoc-svelte";
const tags: Config["tags"] = {
callout: {
// The Svelte component to render the tag
render: "Callout",
// What tags it can have as children
children: ["paragraph", "tag", "list"],
// Define the type of callout through an attribute
attributes: {
type: {
type: String,
default: "note",
matches: ["caution", "check", "note", "warning"],
errorLevel: "critical",
},
title: {
type: String,
},
},
},
};
export default tags;
Then create a Callout component for tag in src/lib/components/Callout.svelte
:
<script lang="ts">
let { title, type, children } = $props();
</script>
<div class={`callout-${type}`}>
<div class="content">
<div class="copy">
<span class="title">{title}</span>
<span>{@render children()}</span>
</div>
</div>
</div>
Then you can use the Callout tag in a Markdoc file:
---
title: Hello World
---
{% callout type="caution" title="Hello" %}
This is a caution callout.
{% /callout %}
Choose whether to turn on typographic replacements from markdown-it.
See the options in action at the markdown-it demo
(select or deselect typographer
).
Defaults to false.
The preprocessor validates whether the Markdoc is valid.
By default, it throws an error on files for issues at the error
or critical
level.
To debug, you can set the level to a lower level to stop the build for any errors at that level or above.
Possible values in ascending order: debug
, info
, warning
, error
, critical
.
Variables are ways to customize your documents at runtime. This way the Markdoc content stays the same, but the generated HTML can vary, such as if you're publishing the same content to various sites.
For example, you might define a $site.name
variable in markdoc/variables.ts
:
import type { Config } from "markdoc-svelte";
const variables: Config["variables"] = {
site: {
name: "Markdoc Svelte",
},
}
export default variables
Then you can use the variable in a Markdoc file:
---
title: Hello World
---
This is published on the {% $site.name %} site.
Markdoc has a few Markdown syntax limitations, see Markdoc FAQ.
To use the enhanced-img plugin with Markdown images, you can customize the default images Node with a custom Svelte component. See the example custom node.
Each proccessed page automatically exports a headings
property with all headings on the page and IDs for each.
Add IDs with annotations or they are generated automatically.
Use this list to generate a table of contents for the page, as in the following example:
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const { frontmatter, headings } = data.page;
// Filter only h1 and h2 headings
const filteredHeadings = headings?.filter((heading) => heading.level <= 2) ?? [];
</script>
<svelte:head>
<title>{data.page.frontmatter?.title ?? 'Undefined title'}</title>
</svelte:head>
{#if filteredHeadings.length > 0}
<ul>
{#each filteredHeadings as heading}
<li>
<a href={`#${heading.id}`}>{heading.text}</a>
</li>
{/each}
</ul>
{/if}
<data.page.default />
Each processed page exports a slug based on the file name. This is a convenient way to generate an index page without reaching into the document.
Glob import these slugs as data for your index page.
For example, if all of your pages end with the extension .md
,
Add the following to src/routes/blog/+page.ts
:
import type { MarkdocModule } from "markdoc-svelte";
import type { PageLoad } from "./$types";
const markdownModules = import.meta.glob("$lib/markdown/*.md");
export const load: PageLoad = async () => {
const content = await Promise.all(
Object.values(markdownModules).map(async (importModule) => {
// Dynamically import each module
const module = (await importModule()) as MarkdocModule;
// Pass only slug and frontmatter to the page data
return {
slug: module.slug,
frontmatter: module.frontmatter,
};
}),
);
return { content };
};
Then use this data to build an index page at src/routes/blog/+page.svelte
:
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const { content } = data;
</script>
<h1>Table of Contents</h1>
<ul>
{#each content as item, i (item.slug)}
<li class={'item-' + i}>
<a href="/{item.slug}">
<h2>{item.frontmatter?.title || item.slug}</h2>
{#if item.frontmatter?.description}
<span>{item.frontmatter.description}</span>
{/if}
{#if item.frontmatter?.published}
<span>{item.frontmatter.published}</span>
{/if}
</a>
</li>
{/each}
</ul>