Add Personalized Content to your Docs with MarkDoc, Next.js, and PropelAuth

Add Personalized Content to your Docs with MarkDoc, Next.js, and PropelAuth

A few days ago, Stripe open sourced Markdoc which is a Markdown-based authoring framework. It’s used to power Stripe’s docs which is already a really strong selling point, but one of the first things that caught my eye was this:

// markdoc/tags.js

import { Button } from '../components/Button';

export const button = {
  render: Button,
};
[//]: <> (pages/docs/example.md)

We can easily add React components to our markdown files:
{% button %}Button Text{% /button %}

(based on this example) You can make your own React components and pretty easily embed them in your markdown files. Let’s look at some interesting ways we can use this.

Using React hooks

We can start off by displaying the current time and using useEffect to keep updating it. Start by following the instructions to set up a new project with Next.js or React. In my case, I used Next.js.

Then we can create our component:

import {useEffect, useState} from "react";

const DateComponent = () => {
    const [date, setDate] = useState(new Date())

    // Update the date every second
    useEffect(() => {
        const timer = setInterval(() => {
            setDate(new Date())
        }, 1000)

        // Clear the timer when we unmount
        return () => { clearInterval(timer) }
    }, [])

    return <div>{date.toLocaleString()}</div>
}

export default DateComponent

Afterwards, in markdoc/tags.js, we export a tag named date which references our DateComponent

import DateComponent from "../components/DateComponent";

export const date = {
    render: DateComponent,
};

And finally, we can create a markdown file (pages/docs/date-example.md) and use the date tag:

# Date tag:

{% date /%}

Pretty easy! If you wanted a more practical use case, you could do something like displaying the age of the post with humanize-duration.

Adding an API Key to your documentation

We can get fancier. Since our component can really do whatever we want, we can fetch information and display it directly in our docs.

One nice feature dev tools documentation often has is the user’s live API key embedded in the docs if they are logged in.

To do this, we can start by just making a fetch to a Next.js API route that we’ll create:

import {useEffect, useState} from "react";

const ApiKey = () => {
    const [apiKey, setApiKey] = useState(null)

    useEffect(() => {
        fetchApiKey(accessToken).then(setApiKey)
    }, [])

    if (apiKey) {
        return <pre className="apiKey">{apiKey}</pre>
    } else {
        return <pre className="apiKey"><Loading/></pre>
    }
}

function fetchApiKey() {
    return fetch("/api/apikey", {
        method: "GET",
    }).then(res => res.text())

}

export default ApiKey

This doesn’t quite make sense though, because we haven’t added in any user information so we won’t know whose API key to fetch. We can use PropelAuth to quickly turn this into an authenticated request either for a user:

import {useAuthInfo, useRedirectFunctions} from "@propelauth/react";
import {useEffect, useState} from "react";

const ApiKey = () => {
    const {loading, isLoggedIn, accessToken} = useAuthInfo()
    const {redirectToLoginPage} = useRedirectFunctions()
    const [apiKey, setApiKey] = useState(null)

    // Check to see if they are logged in before we fetch
    useEffect(() => {
        if (accessToken) {
            fetchApiKey(accessToken).then(setApiKey)
        } else {
            setApiKey(null)
        }
    }, [accessToken])

    // New state: if they aren't logged in display a link
    //   to PropelAuth's hosted login screen so they can login
    if (apiKey) {
        return <pre className="apiKey">{apiKey}</pre>
    } else if (!loading && !isLoggedIn) {
        return <pre className="apiKey">
	          <a href="#" onClick={redirectToLoginPage}>Login</a> to view your API key
	      </pre>
    } else {
        return <pre className="apiKey"><Loading/></pre>
    }
}

// fetchApiKey now takes in an accessToken and passes it in the header
function fetchApiKey(accessToken) {
    return fetch("/api/apikey", {
        method: "GET",
        headers: {"Authorization": `Bearer ${accessToken}`}
    }).then(res => res.text())

}

export default ApiKey

or for an organization that the user is a member of:

const {loading, isLoggedIn, accessToken, orgHelper} = useAuthInfo()

useEffect(() => {
    if (accessToken) {
        // Get the API key for an organization that the user is a member of
        const orgId = orgHelper.getSelectedOrg()
        fetchApiKey(orgId, accessToken).then(setApiKey)
    } else {
        setApiKey(null)
    }
}, [accessToken])

The API route we’ll create to support these use cases will look like:

export default async function handler(req, res) {
    // Verifies that a valid accessToken is provided
    await requireUser(req, res);

    // req.user comes from requireUser
    const apiKey = await fetchApiKeyFromSecretManager(req.user.userId);
    res.status(200).send(apiKey)
}

and you can follow either our Next.js guide or our documentation to learn more and customize it for your use case.

Just like the date tag, we’ll need to add it to markdoc/tags.js, and this markdown file:

# ApiKey tag:

Use your api key with our cool library:

```javascript
myLibrary.init({apiKey: {% apiKey /%}})
```

Produces this:

Summary

The ability to quickly and easily add arbitrary React components to a markdown file is really cool! It allows you to be really creative and easily add dynamic content to your markdown files.