# Build a Markdoc tag

This topic explains how to register custom components that can be invoked by your content creators in Markdoc (Markdown) files.

Follow the tutorial to create two sample components: a simple line break component and a quiz component for some extra credit.

The line break is a very simple component, that adds a linebreak (`<br>`).
In reality, you can achieve the linebreak in Markdoc by ending a line with a `\`.
However, it makes for the simple "Hello World" of custom tags.
And it requires almost no React knowledge.

The quiz component, while a bit rough around the edges, is a lot more robust.
To understand it, you need to understand React.
However, if you do, and want to do more complex behavior, this is a good example to help you understand how to do that.

## Create directories

If you don't have it yet, create a `@theme` directory in your project root.
Inside `@theme`, create a `markdoc` directory.

This is what your directory structure may look after you follow this tutorial.

```treeview
├─ @theme/
|  └── markdoc/
|    ├── components.tsx
|    └── schema.ts
```

## Create your components

Create a file `components.tsx` inside of the `markdoc` directory, and paste the following contents into it.

```tsx
import * as React from 'react';

export function Break() {
  return <br />;
}
```

We'll create the `Quiz.tsx` as extra credit later.

## Create your tags

Create a file `schema.ts` inside of the `markdoc` directory.
Paste the following contents in the file.

```ts
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';

export const tags: Record<string, MarkdocTagSchema> = {
  br: {
    render: 'Break',
    selfClosing: true,
  },
};
```

You've created your first Markdoc tag.
The only thing left to do is use it.

In any file that ends with `.md` add the following `{% br /%}`.

## Add the Quiz component and tag

Add `Quiz` component to the `components.tsx` file.
You can either implement component inline or re-export it from another file.

Let's implement it in `Quiz.tsx` and re-export it from `components.tsx`.

```tsx
import * as React from 'react';

export { Quiz } from './components/Quiz';
export function Break() {
  return <br />;
}
```

This is the directory structure after you've added the `Quiz` component.

```treeview
├─ @theme/
|  ├── markdoc/
|      ├── components.tsx
|      ├── schema.ts
|      └── components/
|          └── Quiz.tsx
```

details
summary
See @theme/markdoc/components/Quiz.tsx
```tsx
import * as React from 'react';
import styled from 'styled-components';

const { useState, useEffect, Fragment } = React;

function Question({ question, setAnswerStatus }) {
  const [selectedAnswerIndex, setSelectedAnswerIndex] = useState(null);

  useEffect(() => {
    if (selectedAnswerIndex != null) {
      setAnswerStatus(selectedAnswerIndex === question.correctAnswerIndex);
    }
  }, [selectedAnswerIndex]);

  useEffect(() => {
    setSelectedAnswerIndex(null);
  }, [question]);

  const getClasses = (index) => {
    let classes = [];
    if (selectedAnswerIndex != null) {
      if (selectedAnswerIndex === index) {
        classes.push('selected');
      }
      if (index === question.correctAnswerIndex) {
        if (selectedAnswerIndex === index) {
          classes.push('correct');
        } else {
          classes.push('incorrect');
        }
      }
    }

    return classes.join(' ');
  };

  return (
    <QuestionEl>
      <QuestionText>{question.question}</QuestionText>
      <Answers>
        {question.answers.map((answer, index) => {
          return (
            <AnswerElement
              key={index}
              className={getClasses(index)}
              onClick={() => selectedAnswerIndex == null && setSelectedAnswerIndex(index)}
            >
              {answer}
            </AnswerElement>
          );
        })}
      </Answers>
    </QuestionEl>
  );
}

function ProgressBar({ currentQuestionIndex, totalQuestionsCount }) {
  const progressPercentage = (currentQuestionIndex / totalQuestionsCount) * 100;

  return (
    <ProgressBarEl>
      <ProgressBarText>
        {currentQuestionIndex} answered ({totalQuestionsCount - currentQuestionIndex} remaining)
      </ProgressBarText>
      <ProgressBarInner style={{ width: `${progressPercentage}%` }} />
    </ProgressBarEl>
  );
}

export function Quiz({ title, summary, questions, children }) {
  const [questionIndex, setQuestionIndex] = useState(null);
  const [answerStatus, setAnswerStatus] = useState(null);
  const [correctAnswerCount, setCorrectAnswerCount] = useState(0);
  const [quizComplete, setQuizComplete] = useState(false);

  useEffect(() => {
    setAnswerStatus(null);
  }, [questionIndex]);

  useEffect(() => {
    if (answerStatus) {
      setCorrectAnswerCount((count) => count + 1);
    }
  }, [answerStatus]);

  const onNextClick = () => {
    if (questionIndex === questions.length - 1) {
      setQuizComplete(true);
    } else {
      setQuestionIndex(questionIndex == null ? 0 : questionIndex + 1);
    }
  };

  const onRestartClick = () => {
    setQuizComplete(false);
    setQuestionIndex(null);
    setCorrectAnswerCount(0);
  };

  if (questionIndex == null) {
    return (
      <Wrapper>
        {title ? <h1>{title}</h1> : null}
        {summary ? <Summary>{summary}</Summary> : null}
        {children}
        <Button className="start" onClick={onNextClick}>
          Start
        </Button>
      </Wrapper>
    );
  }

  return (
    <QuizWrapper>
      {quizComplete ? (
        <Fragment>
          <h1>Quiz complete!</h1>
          <p>
            You answered {correctAnswerCount} questions correctly (out of a total {questions.length}{' '}
            questions)
          </p>
        </Fragment>
      ) : (
        <Fragment>
          <ProgressBar
            currentQuestionIndex={questionIndex}
            totalQuestionsCount={questions.length}
          />
          <Question question={questions[questionIndex]} setAnswerStatus={setAnswerStatus} />
          {answerStatus != null && (
            <div>
              <AnswerStatusEl>
                {!!answerStatus ? 'Correct! :)' : 'Your answer was incorrect :('}
              </AnswerStatusEl>
              <Button className="next" onClick={onNextClick}>
                {questionIndex === questions.length - 1
                  ? 'See results of this quiz'
                  : 'Next Question ->'}
              </Button>
            </div>
          )}
        </Fragment>
      )}

      {questionIndex != null && (
        <Button className="restart" onClick={onRestartClick}>
          Restart quiz
        </Button>
      )}
    </QuizWrapper>
  );
}

const Wrapper = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  width: 600px;
  margin: auto;
`;

const Summary = styled.p`
  margin: 0 0 1rem;
`;

const QuizWrapper = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  width: 600px;
  margin: auto;
`;

const Button = styled.button`
  background: #e8e8e8;
  border: 0;
  padding: 10px 20px;
  cursor: pointer;
  border-bottom: 3px solid #c9c9c9;
  border-radius: 3px;
  &.next {
    background: #6ad85c;
    border-bottom: 3px solid #5abc4e;
  }
  &.start {
    margin-top: 20px;
  }
  &.restart {
    margin-top: 20px;
  }
`;

const QuestionEl = styled.div`
  width: 100%;
`;

const QuestionText = styled.div`
  font-size: 1.2em;
  margin: 20px 0;
`;

const Answers = styled.div`
  margin-bottom: 20px;
`;

const AnswerElement = styled.div`
  padding: 4px;
  text-align: center;
  background: #f3f3f3;
  margin-bottom: 5px;
  border-radius: 3px;
  cursor: pointer;

  &.selected {
    background: gainsboro;
  }

  &.correct {
    background: #6ad85c;
    font-weight: bold;
  }

  &.incorrect {
    background: #df3636;
    font-weight: bold;
  }
`;

const AnswerStatusEl = styled.div`
  font-weight: bold;
  margin-bottom: 20px;
`;

const ProgressBarEl = styled.div`
  width: 100%;
  background: #f3f3f3;
  height: 20px;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 3px;
`;

const ProgressBarInner = styled.div`
  background: #6ad85c;
  position: absolute;
  height: 100%;
  top: 0;
  left: 0;
  transition: ease all 0.5s;
  border-radius: 3px;
`;

const ProgressBarText = styled.div`
  font-size: 0.7em;
  position: absolute;
  z-index: 10;
`;
```

Next, add the quiz tag schema to `schema.ts`

```ts
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';

export const tags: Record<string, MarkdocTagSchema> = {
  br: {
    render: 'Break',
    selfClosing: true,
  },
  quiz: {
    attributes: {
      title: { type: String },
      summary: { type: String },
      questions: {
        type: 'Object',
        required: true,
      },
    },
    render: 'Quiz', // make sure to export it in components.ts,
  },
};
```

You can use the quiz tag.
However, the quiz tag expects a property to define the quiz questions, which is easier to do in the front matter of the Markdown file.
The front matter is passed in as shown below.

```markdoc
---
questions:
  - question: Did you learn how to add a custom Markdoc tag?
    answers:
      - Yes
      - No
      - Maybe
    correctAnswerIndex: 1
  - question: How many places do you need to adjust to configure custom Markdoc tags?
    answers:
      - '0'
      - '1'
      - '2'
      - '3'
      - '4'
    correctAnswerIndex: 4
---

# Hello

Here is my quiz.

{% quiz title="Apply what you learned" summary="Two questions about custom Markdoc tags" questions=$frontmatter.questions %}
Review the tutorial before you begin.
{% /quiz %}
```

We'd show you a working quiz here, but we'd rather spend more time on it and make it look and function better first.

## Expose custom tags for LLMs

Realm generates plain Markdown for LLMs — the `quiz` tag may look interactive in the browser but contribute little text.
Add `renderForLlms` to control what the tag emits for LLMs.

### `renderForLlms`

The function receives the Markdoc AST `node` for your tag and a context object.
It should return a plain Markdown string.
The tag body is not included unless you call `getBody()` from the context object.

Security considerations
Content returned by `renderForLlms` is emitted directly for LLMs.
Only return text you are comfortable making public — do not pull secrets, API keys, tokens, or other sensitive information.

#### Context

| Field | Type | Description |
|  --- | --- | --- |
| `getBody` | () => string | Returns plain Markdown from the tag body when called. |


### Examples

The examples below add `renderForLlms` to the `quiz` tag from this tutorial.

If you are updating an existing `schema.ts` file, replace `Schema` with `MarkdocTagSchema`.
`renderForLlms` is defined on `MarkdocTagSchema`, so keeping `Schema` can cause TypeScript errors.

```markdoc
{% quiz title="Apply what you learned" summary="Two questions about custom Markdoc tags" questions=$frontmatter.questions %}
Review the tutorial before you begin.
{% /quiz %}
```

#### Summary only

Return the `summary` attribute and omit the tag body.

```ts
import type { Node } from '@markdoc/markdoc';
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';

export const tags: Record<string, MarkdocTagSchema> = {
  quiz: {
    attributes: {
      title: { type: String },
      summary: { type: String },
      questions: { type: 'Object', required: true },
    },
    render: 'Quiz',
    renderForLlms: (node: Node) => node.attributes.summary ?? '',
  },
};
```

LLM output:

```md
Two questions about custom Markdoc tags
```

#### Full section

Return the `title` and `summary` attribute values, then append the tag body from `getBody()`.

```ts
import type { Node } from '@markdoc/markdoc';
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';

export const tags: Record<string, MarkdocTagSchema> = {
  quiz: {
    attributes: {
      title: { type: String },
      summary: { type: String },
      questions: { type: 'Object', required: true },
    },
    render: 'Quiz',
    renderForLlms: (node: Node, { getBody }) =>
      [node.attributes.title, node.attributes.summary, getBody()].filter(Boolean).join('\n'),
  },
};
```

LLM output:

```md
Apply what you learned
Two questions about custom Markdoc tags
Review the tutorial before you begin.
```

## Extra credit

Make a custom tag of your own (it could be simple).
Our philosophy is in line with Confucius:

> I hear and I forget, I see and I remember, I do and I understand.


Go do!

## Resources

- **[Markdoc tags](/docs/realm/content/markdoc-tags)** - See the full list of supported Markdoc tags
- **[Markdoc overview for technical writers](https://redocly.com/learn/markdoc)** - Learn how to use Markdoc in your documentation
- **[Customization](/docs/realm/customization)** - Discover customizable components and customization options for your project