contentlayerdev / contentlayer

Contentlayer turns your content into data - making it super easy to import MD(X) and CMS content in your app
https://www.contentlayer.dev
MIT License
3.3k stars 203 forks source link

Investigate table of content use case #137

Open schickling opened 2 years ago

schickling commented 2 years ago

My first approach would be to try to use computed fields for this.

Related

schickling commented 2 years ago

As an initial "user-land" solution I've come up with the following:

import { defineDocumentType } from 'contentlayer/source-files'
import type * as unified from 'unified'
import { toMarkdown } from 'mdast-util-to-markdown'
import { mdxToMarkdown } from 'mdast-util-mdx'

import { bundleMDX } from 'mdx-bundler'

export type DocHeading = { level: 1 | 2 | 3; title: string }

export const Doc = defineDocumentType(() => ({
  name: 'Doc',
  filePathPattern: `docs/**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      description: 'The title of the page',
      required: true,
    },
  },
  computedFields: {
    headings: {
      type: 'json',
      resolve: async (doc) => {
        const headings: DocHeading[] = []

        await bundleMDX({
          source: doc.body.raw,
          xdmOptions: (opts) => {
            opts.remarkPlugins = [...(opts.remarkPlugins ?? []), tocPlugin(headings)]
            return opts
          },
        })

        return [{ level: 1, title: doc.title }, ...headings]
      },
    },
  },
}))

const tocPlugin =
  (headings: DocHeading[]): unified.Plugin =>
  () => {
    return (node: any) => {
      node.children
        .filter((_: any) => _.type === 'heading')
        .forEach((heading: any) => {
          const title = toMarkdown({ type: 'paragraph', children: heading.children }, { extensions: [mdxToMarkdown()] })
            .trim()
            // removes MDX in headlines
            .replace(/<.*$/g, '')
            .trim()

          return headings.push({ level: heading.depth, title })
        })
    }
  }

This is definitely not an ideal solution and should be improved further. One approach could be to make this an "out of the box" feature of Contentlayer.

schickling commented 2 years ago

A more elegant foundation for this feature could be #216.

Clarity-89 commented 1 year ago

I think it'd be great if the TOC would be available separately, and not as a part of the body. This is a current limitation of Remark-toc, which doesn't work when you want the TOC to be rendered outside of content. I've made a custom solution for this, but would be great if it was built-in into Contentlayer.