---
title: Markdown and MDX | Guides
description: Learn how to use Markdown and MDX in Qwik Router.
contributors:
  - manucorporat
  - adamdbradley
  - Oyemade
  - mhevery
  - nnelgxorz
  - cunzaizhuyi
  - saisrikardumpeti
  - zanettin
  - ihaback
  - YannickFricke
  - mrhoodz
  - erikras
  - hamatoyogi
updated_at: '2023-10-17T20:22:18Z'
created_at: '2023-03-20T23:45:13Z'
---

# MDX

An alternative way to author content is using `.mdx` files ([Markdown JSX](https://mdxjs.com/)). These files are authored as Markdown, but they are compiled down to Qwik components. In addition to Markdown syntax, `.mdx` files can also refer to other components.

Let's assume you have your routes set up like this:

```bash
src/
└── routes/
    └── some/
        └── path/
            └── index.mdx    # https://example.com/some/path
```

```markdown title="src/routes/some/path/index.mdx"
---
title: Hello World Title
---

This is a simple hello world component.

```

The above component will be rendered at `https://example.com/some/path`.

## Importing other components.

[MDX](https://mdxjs.com/) is a creative opportunity for you to come up with new content quickly ("Qwikly" 🙂) and if you need more interaction on your page you can seamlessly integrate your Qwik components like so:

```bash
src/
├── components/
|   └── counter
│       └──  counter.tsx
└── routes/
    └── some/
        └── path/
            └── index.mdx    # https://example.com/some/path
```

```markdown title="src/routes/some/path/index.mdx"
---
title: Hello World Title
---
import { Counter } from "../../../components/counter/counter";

This is a simple hello world component.

<Counter />

```

```tsx title="src/components/counter/counter.tsx"
import { component$, useSignal } from '@qwik.dev/core';

export const Counter = component$(() => {
  const count = useSignal(0);

  return (
    <button class="counter" type="button" onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});
```

**Note**: A key difference between Qwik Router and some other meta-frameworks' routers is directory-based routing. Every route needs to be defined as `a-directory/index.(tsx,ts,js,jsx,md,mdx)`.

In other meta-frameworks' routers `about.mdx` might render the route `http://example.com/about`. However, this will not work in the Qwik Router. You must rename the file to `about/index.mdx` for Qwik Router to know to render it.

## Disabling default MDX plugins included.

Qwik Router includes 3 plugins by default.

- [remarkGfm](https://github.com/remarkjs/remark-gfm): [GFM](https://github.github.com/gfm/) support (autolink literals, footnotes, strikethrough, tables, tasklists).
- [rehypeSyntaxHighlight](https://github.com/wooorm/refractor): Lightweight, robust, elegant virtual syntax highlighting using Prism.
- [rehypeAutolinkHeadings](https://github.com/rehypejs/rehype-autolink-headings): Plugin to add links to headings in HTML.

These plugins can be disabled independently in the following way:

```tsx title="vite.config.js"
import { defineConfig } from 'vite';
import { qwikRouter } from '@qwik.dev/router/vite';
// See below
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
export default defineConfig(() => {
  return {
    plugins: [
      qwikRouter({
        mdxPlugins: {
          remarkGfm: false,
          rehypeSyntaxHighlight: false,
          rehypeAutolinkHeadings: false,
        },
        mdx: {
          rehypePlugins: [
            // Plugins can now be added manually to use a different configuration
            [rehypeAutolinkHeadings, { behavior: 'wrap' }],
          ],
        },
      }),
      /* the rest of the configuration */
    ],
  };
});
```

## Open Graph Properties

You can use `og` or `opengraph` property to define [Open Graph protocol](https://ogp.me/) metadata.

```yaml
title: My Title
description: My Description
og:
  - title: My Custom Title
    description: true
  - image: https://example.com/rock.jpg
    image:alt: A shiny red apple with a bite taken out
  - image: https://example.com/rock2.jpg
```

Setting `og:title` or `og:description` to `true` will check and use outside `title` and `description` property instead. Thus, you can avoid writing a same title and description twice.

The above example will generate the following HTML code.

```html
<title>My Title</title>
<meta name="description" content="My Description" />

<meta property="og:title" content="My Custom Title" />
<meta property="og:description" content="My Description" />

<meta property="og:image" content="https://example.com/rock.jpg" />
<meta property="og:image:alt" content="A shiny red apple with a bite taken out" />

<meta property="og:image" content="https://example.com/rock2.jpg" />
```

## Reading frontmatter data
Frontmatter keys are accessible by leveraging the `useDocumentHead()` hook.

```markdown title="src/routes/some/path/index.mdx"
---
title: Hello World Title
tags:
  - super
  - exiting
  - docs
---
import { Tags } from "../../../components/tags/tags";

This is a simple hello world component.

<Tags />
```

```tsx title="src/components/tags/tags.tsx"
import { component$ } from '@qwik.dev/core';
import { useDocumentHead } from '@qwik.dev/router';

export const Tags = component$(() => {
  const { frontmatter } = useDocumentHead();
  
  if (frontmatter.tags.length === 0) {
    return null;
  }
  
  return (
    <ul>
      {frontmatter.tags.map((tag: string) => (
        <li key={`tag-${tag}`}>{tag}</li>
      ))}
    </ul>
  );
});
```

## `useContent()`

The [`useContent()`](/docs/(qwikrouter)/api/index.mdx#usecontent) function retrieves the nearest content information for the current route. The returned object includes:

```ts
headings: ContentHeading[] | undefined;
menu: ContentMenu | undefined;
```

The `headings` array includes data about a markdown file's `<h1>` to `<h6>` [html heading elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements).

Menus are contextual data declared with `menu.md` files. See [menus file definition](/docs/(qwikrouter)/advanced/menu/index.mdx) for more information on the file format and location.

# Dynamic Page Navigation with MDX

When working with documentation or content-heavy pages in Qwik City, you often need to generate a table of contents or sidebar navigation based on the page's content. Qwik City provides a built-in solution for this through the `useContent()` hook, which can automatically extract headings from your MDX files.

## Using `useContent()` for Page Navigation

The `useContent()` hook allows you to access metadata about your current MDX page, including all its headings. This is particularly useful for:

- Creating a table of contents for long articles
- Building dynamic sidebar navigation
- Implementing "jump to section" functionality
- Generating progress indicators for article sections

Here's a complete example of how to create a dynamic table of contents:

```tsx
import { component$, useContent } from '@builder.io/qwik';

export const TableOfContents = component$(() => {
  const content = useContent();

  return (
    <nav class="toc">
      <h4>On this page</h4>
      <ul>
        {content.headings?.map((heading) => (
          <li 
            key={heading.id}
            style={{ 
              // Indent based on heading level
              marginLeft: `${(heading.level - 1) * 12}px` 
            }}
          >
            <a href={`#${heading.id}`}>{heading.text}</a>
          </li>
        ))}
      </ul>
    </nav>
  );
});
```

## Understanding the Headings Data

The `headings` property from `useContent()` provides an array of heading objects with the following information:

- `id`: The auto-generated ID for the heading (used for anchor links)
- `text`: The actual text content of the heading
- `level`: The heading level (1 for h1, 2 for h2, etc.)

This only works with `.mdx` files - headings in `.tsx` files are not detected.

## Common Use Cases

### Progressive Disclosure Navigation

You can create a collapsible navigation that shows the current section and its sub-sections:

```tsx
export const ProgressiveNav = component$(() => {
  const content = useContent();
  const currentSection = useSignal<string | null>(null);

  return (
    <nav>
      {content.headings?.map((heading) => {
        if (heading.level === 2) { // Only show h2 as main sections
          const subHeadings = content.headings.filter(h => 
            h.level === 3 && 
            h.id.startsWith(heading.id.split('-')[0])
          );
          
          return (
            <div key={heading.id}>
              <a 
                href={`#${heading.id}`}
                onClick$={() => currentSection.value = heading.id}
              >
                {heading.text}
              </a>
              {currentSection.value === heading.id && (
                <ul>
                  {subHeadings.map(sub => (
                    <li key={sub.id}>
                      <a href={`#${sub.id}`}>{sub.text}</a>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          );
        }
      })}
    </nav>
  );
});
```

### Reading Progress Indicator

You can combine heading information with scroll position to create a reading progress indicator:

```tsx
export const ReadingProgress = component$(() => {
  const content = useContent();
  const activeSection = useSignal('');

  useOnWindow('scroll', $(() => {
    const headingElements = content.headings?.map(h => 
      document.getElementById(h.id)
    ).filter(Boolean) || [];

    const currentHeading = headingElements.find(el => {
      const rect = el!.getBoundingClientRect();
      return rect.top > 0 && rect.top < window.innerHeight / 2;
    });

    if (currentHeading) {
      activeSection.value = currentHeading.id;
    }
  }));

  return (
    <nav>
      {content.headings?.map(heading => (
        <a
          key={heading.id}
          href={`#${heading.id}`}
          class={{
            active: activeSection.value === heading.id
          }}
        >
          {heading.text}
        </a>
      ))}
    </nav>
  );
});
```

## Tips and Best Practices

1. **Consistent Heading Structure**: Maintain a logical heading hierarchy in your MDX files to ensure the navigation makes sense.

2. **Performance**: The `useContent()` hook is optimized and won't cause unnecessary re-renders, so you can safely use it in navigation components.

3. **Styling**: Consider using the heading level information to create visual hierarchy in your navigation:
   ```css
   .toc a {
     /* Base styles */
   }
   
   /* Style based on heading level */
   [data-level="1"] { font-size: 1.2em; font-weight: bold; }
   [data-level="2"] { font-size: 1.1em; }
   [data-level="3"] { font-size: 1em; }
   ```

4. **Accessibility**: Always ensure your dynamic navigation includes proper ARIA labels and keyboard navigation support.

## Notes and Limitations

- This functionality only works with `.mdx` files, not with `.tsx` or other file types
- Headings must have unique content to generate unique IDs
- The heading data is available only on the client-side after hydration
- Consider using `useVisibleTask$` if you need to interact with the heading elements in the DOM