DEV Community

Cover image for Getting started with Markdoc in Next.js
Charlie Gerard for Stripe

Posted on

Getting started with Markdoc in Next.js

Stripe is open-sourcing Markdoc, the Markdown-based authoring system that powers the Stripe documentation website. Whether it’s for a simple static site, authoring tooling, or a full-blown documentation website, Markdoc is designed to grow with you, so you can create engaging experiences no matter how large or complex your documentation becomes. Let’s have a look at how to get started.
The rest of this tutorial is implemented in Next.js, using create-next-app.

Installation

To start using Markdoc, you first need to install it with:

npm install @markdoc/markdoc --save
Enter fullscreen mode Exit fullscreen mode

or

yarn add @markdoc/markdoc –-save
Enter fullscreen mode Exit fullscreen mode

As this sample app is using Next.js, you also need to install the Next.js plugin:

npm install @markdoc/next.js --save
Enter fullscreen mode Exit fullscreen mode

Finally, add the following lines to the next.config.js file:

const withMarkdoc = require("@markdoc/next.js");
module.exports = withMarkdoc()({
 pageExtensions: ["js", "md", "mdoc"],
});
Enter fullscreen mode Exit fullscreen mode

These lines allow you to use .js, .md and .mdoc files.

These additions to the config file enable you to write your docs in either JavaScript or Markdown file formats. If you would like to use Markdown files with another framework than Next.js, a plugin will be required.

Now that you’re all set up, let’s write some content.

Using Markdoc

To get started, locate the pages folder that’s automatically generated when spinning up a Next.js app with create-next-app, and create a new index.md file inside it.

Markdoc syntax is a superset of Markdown, specifically the CommonMark specification, so, in this file, you can write content using this syntax.

# Some title
## A subtitle
This is a paragraph with a [link to your awesome website](https://your-awesome-website.com)
Enter fullscreen mode Exit fullscreen mode

Markdoc is extensible so you can also use variables, functions, and create custom tags. For these, you have to define your content in JavaScript and use Markdoc.transform() and Markdoc.renderers.react() to render everything.
Let’s look into how to do that.

Variables

Variables are defined in a config object and can then be used in your content. Here’s what a complete code sample could look like before we break it down and explain the different pieces.

// config.js
export const config = {
 variables: {
   user: {
     name: "Justice Ketanji Brown Jackson",
   },
 },
};
Enter fullscreen mode Exit fullscreen mode
// Page.js
import React from "react";
import Markdoc from "@markdoc/markdoc";
import { config } from "./config.js";

const Page = () => {
 const doc = `
 Hello {% $user.name %}
 `;

 const content= Markdoc.transform(doc, config);

 return <section>{Markdoc.renderers.react(content, React)}</section>;
};

export default Page;
Enter fullscreen mode Exit fullscreen mode

For example, if you wanted to display a user’s name, you would declare a config object like this:

 const config = {
   variables: {
     user: {
       name: 'Justice Ketanji Brown Jackson'
     }
   }
 };
Enter fullscreen mode Exit fullscreen mode

In Markdoc, to reference the variables in your config, prepend the variable name with a $dollarSign. For example, you would refer to the user’s name like this:

const content = `
Hello {% $user.name %}
`;
Enter fullscreen mode Exit fullscreen mode

Don’t forget to prepend the variable with a $, otherwise it will be interpreted as a tag.

Then, you need to pass these two variables in Markdoc.transform() and render your content, using Markdoc.renderers.react().

const content= Markdoc.transform(doc, config);

return <section>{Markdoc.renderers.react(content, React)}</section>;
Enter fullscreen mode Exit fullscreen mode

Using variables is a powerful feature, for instance, if you want to display dynamic data, such as a user’s API key. An example of such a feature can be found on the Stripe documentation website.

Customizing styles

Markdoc introduces the concept of annotations to allow you to style different nodes, which are elements Markdoc inherits from Markdown.

For example, you can add IDs and classes with the following syntax:

// index.md
# My title {% #custom-id %}
# Another title {% .custom-class-name-here %}
Enter fullscreen mode Exit fullscreen mode

You can then refer to these in your CSS to apply styles.

// styles.css
#custom-id {
 color: purple;
}
.custom-class-name-here {
 color: darkblue;
}
Enter fullscreen mode Exit fullscreen mode

This would generate the following HTML:

<h1 id="custom-id">My title </h1>
<h1 class="custom-class-name-here">Another title </h1>
Enter fullscreen mode Exit fullscreen mode

And render the content shown below:

Screenshot of a purple title with a dark blue title underneath

Some style-related attributes can also be applied using annotations:

Function {% width="25%" %}
Example  {% align="right" %}
Enter fullscreen mode Exit fullscreen mode

Tags

Markdoc comes with four built-in tags and also lets you build your own. For example, you can try the if tag that lets you conditionally render content.

const config = {
 variables: {
   tags: {
     featureFlagEnabled: true,
   },
 },
};

const document = `
{% if $tags.featureFlagEnabled %}

Here's some secret content

{% /if %}
`;
Enter fullscreen mode Exit fullscreen mode

You can build your own tags in three steps. Let’s go through an example with a custom banner component.
First, you need to start by creating the React component that you want to render. The code for a small banner component could look like this:

// Banner.js
const Banner = ({ type, children }) => {
 return (
   <section className={`banner ${type}`}>
     {children}
     <style jsx>{`
       .alert {
         border: 1px solid red;
       }
     `}</style>
   </section>
 );
};

export default Banner;
Enter fullscreen mode Exit fullscreen mode

This component will accept a type prop to indicate if the banner should be styled as an alert, info, warning banner, etc. The children prop represents the content that will be passed inside that tag in the Markdown file later on.

To be able to use this component as a tag in a Markdown file, first create a “markdoc” folder at the root of your Next.js app, and a “tags.js” file inside it, that will contain all your custom tags.

This app’s architecture would end up looking like this:

components/
|-- Banner.js
markdoc/
|-- tags.js
pages/
   index.md
Enter fullscreen mode Exit fullscreen mode

Inside your custom tag file (here tags.js), you need to import your React component and export a variable containing the component you want to display. You would also include any attributes you want to use. In this case, the type of banner.

When declaring the attributes, you need to specify their data type.

// markdoc/tags.js
import Banner from "../components/Banner";

export const banner = {
 Component: Banner,
 attributes: {
   type: {
     type: String,
   },
 },
};
Enter fullscreen mode Exit fullscreen mode

The final step is to use this custom tag in your Markdown content, like this:

{% banner type="alert" %}
This is an alert banner
{% /banner %}
Enter fullscreen mode Exit fullscreen mode

If you create a custom tag that does not accept any children, you can write it as a self-closing tag:

{% banner/ %}
Enter fullscreen mode Exit fullscreen mode

Syntax validation

Additionally, Markdoc lets you validate the abstract syntax tree (AST) generated before rendering. If you consider the Banner component written above, you can use it as a tag when writing your content in JavaScript and check for any syntax error before rendering.

For example, if a banner tag is used without a type attribute that is required, you can implement some error handling to avoid rendering broken content.
This syntax validation can be implemented with a single line, using Markdoc.validate().

 const config = {
   tags: {
     banner,
   },
 };

 const content = `
 {% banner %}
 Example banner with a syntax error
 {% /banner %}
`;

 const ast = Markdoc.parse(content);
 const errors = Markdoc.validate(ast, config);
 // Handle errors
Enter fullscreen mode Exit fullscreen mode

In this case, the error returned will look like this.

Screenshot of an error in the browser's dev tools with a message saying "Missing required attribute 'type'"

Functions

You can extend Markdoc with custom utilities by writing your own functions. For example, if you wanted to add a way to transform your content to uppercase, you would start by creating a file inside your markdoc folder, for example functions.js. In this file, add the following helper function:

// markdoc/functions.js

export const uppercase = {
 transform(parameters) {
   const string = parameters[0];
   return typeof string === 'string' ? string.toUpperCase() : string;
 }
};
Enter fullscreen mode Exit fullscreen mode

Then import this function in the component that needs it, and pass it in your config object:

import { uppercase } from "../markdoc/functions";

const config = {
 functions: {
   uppercase
 }
};
Enter fullscreen mode Exit fullscreen mode

Call it in your content within {% %} tags:

const document = `
 {% uppercase("Hello, World") %}
`
Enter fullscreen mode Exit fullscreen mode

And finally, call Markdoc.transform() and use the React renderer to render everything.

const doc = Markdoc.transform(document, config);
Markdoc.renderers.react(doc, React);
Enter fullscreen mode Exit fullscreen mode

So, the complete syntax for a small component would look like this:

// config.js
export const config = {
 functions: { uppercase },
};
Enter fullscreen mode Exit fullscreen mode
// functions.js
export const uppercase = {
   transform(parameters) {
     const string = parameters[0];
     return typeof string === "string" ? string.toUpperCase() : string;
   },
 };
Enter fullscreen mode Exit fullscreen mode
// Page.js
import Markdoc from "@markdoc/markdoc";
import { config } from "./config.js";
import { uppercase } from "./functions.js";

const Page = () => {
 const document = `
 {% uppercase("Hello, World") %}
 `;

 const doc = Markdoc.transform(document, config);

 return <section>{Markdoc.renderers.react(doc, React)}</section>;
};

export default Page;
Enter fullscreen mode Exit fullscreen mode

Some built-in functions are available such as equals, to display some content if two variables are equal, not/and/or to use in an if statement to perform boolean operations, default that returns the second parameter passed if the first returns undefined, and debug that serializes the values as JSON to help you while debugging.

Resources

If you’re interested in learning more, here are some useful resources you can check out, and if you’re interested, we welcome your contributions to the repository! We can’t wait to see what you’re going to build with Markdoc and don’t hesitate to let us know what you think!

Official Markdoc documentation
Markdoc repository
Markdoc Next.js plugin repository
Markdoc playground
Next.js boilerplate demo

Stay connected

In addition, you can stay up to date with Stripe in a few ways:
📣 Follow us on Twitter
💬 Join the official Discord server
📺 Subscribe to our Youtube channel
📧 Sign up for the Dev Digest

Discussion (1)

Collapse
booboboston profile image
Bobo Brussels

Thanks for writing this.