Guides / Frontmatter
This guide explores how to support YAML frontmatter in MDX. MDX supports standard markdown syntax (CommonMark). That means frontmatter is not supported by default.
MDX comes with a powerful and dynamic alternative to frontmatter, namely ESM (import
/export
). These exports:
export const name = 'World'
export const title = 'Hi, ' + name + '!'
# {title}
Can be used like so:
import * as Post from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
console.log(Post.title) // Prints 'Hi, World!'
(alias) module "*.mdx"
import Post
An MDX file which exports a JSX component.
The default export of MDX files is a function which takes props and returns a JSX element. MDX files can export other identifiers from within the MDX file as well, either authored manually or automatically through plugins
It’s currently not possible for the other exports to be typed automatically. You can type them yourself with a TypeScript script which augments *.mdx
modules. A script file is a file which doesn’t use top-level ESM syntax, but ESM syntax is allowed inside the declared module.
This is typically useful for exports created by plugins. For example:
// mdx-custom.d.ts
declare module '*.mdx' {
import { Frontmatter } from 'my-frontmatter-types';
export const frontmatter: Frontmatter;
export const title: string;
}
The previous example added types to all .mdx
files. To define types for a specific MDX file, create a file with the same name but postfixed with .d.ts
next to the MDX file.
For example, given the following MDX file my-component.mdx
:
export const message = 'world';
# Hello {message}
Create the following file named my-component.mdx.d.ts
in the same directory:
export { default } from '*.mdx';
export const message: string;
Note that this overwrites the declare module '*.mdx' { … }
types from earlier, which is why you also need to define the default export. You can also define your own default export type to narrow the accepted prop types of this specific file.
It should now be possible to import both the MDX component and the exported constant message
.
namespace console
var console: Console
The console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without calling require('console')
.Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
(method) Console.log(message?: any, ...optionalParams: any[]): void
Prints to stdout
with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
(alias) module "*.mdx"
import Post
An MDX file which exports a JSX component.
The default export of MDX files is a function which takes props and returns a JSX element. MDX files can export other identifiers from within the MDX file as well, either authored manually or automatically through plugins
It’s currently not possible for the other exports to be typed automatically. You can type them yourself with a TypeScript script which augments *.mdx
modules. A script file is a file which doesn’t use top-level ESM syntax, but ESM syntax is allowed inside the declared module.
This is typically useful for exports created by plugins. For example:
// mdx-custom.d.ts
declare module '*.mdx' {
import { Frontmatter } from 'my-frontmatter-types';
export const frontmatter: Frontmatter;
export const title: string;
}
The previous example added types to all .mdx
files. To define types for a specific MDX file, create a file with the same name but postfixed with .d.ts
next to the MDX file.
For example, given the following MDX file my-component.mdx
:
export const message = 'world';
# Hello {message}
Create the following file named my-component.mdx.d.ts
in the same directory:
export { default } from '*.mdx';
export const message: string;
Note that this overwrites the declare module '*.mdx' { … }
types from earlier, which is why you also need to define the default export. You can also define your own default export type to narrow the accepted prop types of this specific file.
It should now be possible to import both the MDX component and the exported constant message
.
const title: string
You might prefer frontmatter though, as it lets you define data that can be extracted from the file system before compiling. Say our MDX with frontmatter looked like this:
---
title: Hi, World!
---
# Hi, World!
Then without compiling or evaluating the metadata can be accessed like so:
import {read} from 'to-vfile'
import {matter} from 'vfile-matter'
const file = await read('example.mdx')
matter(file)
console.log(file.data.matter)
(alias) function read(description: Compatible, options: BufferEncoding | ReadOptions | null | undefined, callback: Callback): undefined (+2 overloads)
import read
(alias) function matter(file: VFile, options?: Options | null | undefined): undefined
import matter
Parse the YAML front matter in a file and expose it as file.data.matter
.
If no matter is found in the file, nothing happens, except that file.data.matter
is set to an empty object ({}
).
If the file value is an Uint8Array
, assumes it is encoded in UTF-8.
const file: VFile
(alias) read(description: Compatible, options?: BufferEncoding | ReadOptions | null | undefined): Promise<VFile> (+2 overloads)
import read
(alias) matter(file: VFile, options?: Options | null | undefined): undefined
import matter
Parse the YAML front matter in a file and expose it as file.data.matter
.
If no matter is found in the file, nothing happens, except that file.data.matter
is set to an empty object ({}
).
If the file value is an Uint8Array
, assumes it is encoded in UTF-8.
const file: VFile
namespace console
var console: Console
The console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without calling require('console')
.Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
(method) Console.log(message?: any, ...optionalParams: any[]): void
Prints to stdout
with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
const file: VFile
(property) VFile.data: Data
Place to store custom info (default: {}
).
It’s OK to store custom data directly on the file but moving it to data
is recommended.
unknown
Our compiler, @mdx-js/mdx
, doesn’t understand YAML frontmatter by default but it can be enabled by using a remark plugin, remark-frontmatter
:
import fs from 'node:fs/promises'
import {compile} from '@mdx-js/mdx'
import remarkFrontmatter from 'remark-frontmatter'
const file = await compile(await fs.readFile('example.mdx'), {
remarkPlugins: [remarkFrontmatter]
})
console.log(file)
(alias) module "node:fs/promises"
import fs
(alias) function compile(vfileCompatible: Readonly<Compatible>, compileOptions?: Readonly<CompileOptions> | null | undefined): Promise<VFile>
import compile
Compile MDX to JS.
(alias) function remarkFrontmatter(options?: Options | null | undefined): undefined
import remarkFrontmatter
Add support for frontmatter.
Doesn’t parse the data inside them: create your own plugin to do that.
'yaml'
).const file: VFile
(alias) compile(vfileCompatible: Readonly<Compatible>, compileOptions?: Readonly<CompileOptions> | null | undefined): Promise<VFile>
import compile
Compile MDX to JS.
(alias) module "node:fs/promises"
import fs
function readFile(path: PathLike | fs.FileHandle, options?: ({
encoding?: null | undefined;
flag?: OpenMode | undefined;
} & EventEmitter<T extends EventMap<T> = DefaultEventMap>.Abortable) | null): Promise<Buffer> (+2 overloads)
Asynchronously reads the entire contents of a file.
If no encoding is specified (using options.encoding
), the data is returned as a Buffer
object. Otherwise, the data will be a string.
If options
is a string, then it specifies the encoding.
When the path
is a directory, the behavior of fsPromises.readFile()
is platform-specific. On macOS, Linux, and Windows, the promise will be rejected with an error. On FreeBSD, a representation of the directory's contents will be returned.
An example of reading a package.json
file located in the same directory of the running code:
import { readFile } from 'node:fs/promises';
try {
const filePath = new URL('./package.json', import.meta.url);
const contents = await readFile(filePath, { encoding: 'utf8' });
console.log(contents);
} catch (err) {
console.error(err.message);
}
It is possible to abort an ongoing readFile
using an AbortSignal
. If a request is aborted the promise returned is rejected with an AbortError
:
import { readFile } from 'node:fs/promises';
try {
const controller = new AbortController();
const { signal } = controller;
const promise = readFile(fileName, { signal });
// Abort the request before the promise settles.
controller.abort();
await promise;
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err);
}
Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.readFile
performs.
Any specified FileHandle
has to support reading.
FileHandle
(property) remarkPlugins?: PluggableList | null | undefined
List of remark plugins (optional).
(alias) function remarkFrontmatter(options?: Options | null | undefined): undefined
import remarkFrontmatter
Add support for frontmatter.
Doesn’t parse the data inside them: create your own plugin to do that.
'yaml'
).namespace console
var console: Console
The console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without calling require('console')
.Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
(method) Console.log(message?: any, ...optionalParams: any[]): void
Prints to stdout
with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
const file: VFile
Now it “works”. The frontmatter is not rendered as if it was markdown. But the data embedded in the frontmatter isn’t available from inside the MDX. What if we wanted that too? Like so:
---
title: Hi, World!
---
# {title}
That’s exactly what the remark plugin remark-mdx-frontmatter
does.
That plugin, like all remark plugins, can be passed as remarkPlugins
in ProcessorOptions
. More info on plugins is available in § Extending MDX