Giter VIP home page Giter VIP logo

next-mdx's Introduction

⚠️ This project has been retired. Consider using next-mdx-remote or mdx-bundler or nextra.

next-mdx

Test License PRs welcome! Follow @shadcn

next-mdx provides a set of helper functions for fetching and rendering local MDX files. It handles relational data, supports custom components, TypeScript ready and is really fast.

next-mdx is great for building mdx-powered pages, multi-user blogs, category pages..etc.

next-mdx-relational-data

Table of Contents

Demo

https://next-mdx-example.vercel.app

Quick Start

Learn how next-mdx works by looking at examples.

  1. Go to example-page
  2. Open next-mdx.json to see the sample configuration.
  3. Open pages/[[...slug]].tsx to see how MDX files are fetched and rendered.
  4. See types/index.d.ts for TypeScript.

Examples

Click to expand examples.

next-mdx.json
{
  "post": {
    "contentPath": "content/posts",
    "basePath": "/blog",
    "sortBy": "date",
    "sortOrder": "desc"
  },
}
pages/posts/[...slug].jsx
import { useHydrate } from "next-mdx/client"
import { getMdxNode, getMdxPaths } from "next-mdx/server"

export default function PostPage({ post }) {
const content = useHydrate(post)

  return (
    <article>
      <h1 variant="heading.title">{post.frontMatter.title}</h1>
      {post.frontMatter.excerpt ? (
        <p variant="text.lead" mt="4">
          {post.frontMatter.excerpt}
        </p>
      ) : null}
      <hr />
      {content}
    </article>
  )

}

export async function getStaticPaths() {
return {
paths: await getMdxPaths("post"),
fallback: false,
}
}

export async function getStaticProps(context) {
const post = await getMdxNode("post", context)

  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: {
      post,
    },
  }

}

Installation


npm i --save next-mdx

Configuration

Create a next-mdx.json file at the root of your project with the following:

{
  "post": {
    "contentPath": "content/posts",
    "basePath": "/blog",
    "sortBy": "date",
    "sortOrder": "desc"
  },
  "category": {
    "contentPath": "content/categories"
  }
}
  1. post, category and author keys are unique IDs used as references for your MDX types.
  2. contentPath (required) is where your MDX files are located.
  3. basePath (optional) is the path used for generating URLs.
  4. sortBy (optional, defaults to title) is the name of the frontMatter field used for sorting.
  5. sortOrder (optional, defaults to asc) is the sorting order.

Reference

next-mdx exposes 6 main helper functions:

  • getMdxPaths(sourceName: string)
  • getNode(sourceName, context)
  • getAllNodes(sourceName)
  • getMdxNode(sourceName, context, params)
  • getAllMdxNodes(sourceName, params)
  • useHydrate(node, params)

getMdxPaths

getMdxPaths(sourceName: string) returns an array of path params which can be passed directly to paths in getStaticPaths`.

  • sourceName is the unique ID defined in next-mdx.json

Example

// pages/blog/[...slug].js
import { getMdxPaths } from "next-mdx/server"

export async function getStaticPaths() {
  return {
    paths: await getMdxPaths("post"),
    fallback: false,
  }
}

getNode

getNode(sourceName, context) returns an MDXNode with frontMatter and relational data but without MDX data. This is really fast and cached.

Use this instead of getMdxNode if you are not rendering MDX content on a page.

  • sourceName is the unique ID defined in next-mdx.json
  • context is the context passed to getStaticProps or the slug as a string.

Example

// pages/blog/[...slug].js
import { getNode } from "next-mdx/server"

export async function getStaticProps(context) {
  const post = await getNode("post", context)

  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: {
      post,
    },
  }
}

getAllNodes

getAllNodes(sourceName) returns all MdxNode of the given type/source with frontMatter and relational data but without MDX data. This is also really fast and cached.

  • sourceName is the unique ID defined in next-mdx.json

Example

import { getAllNodes } from "next-mdx/server"

export async function getStaticProps() {
  return {
    props: {
      posts: await getAllNodes("post"),
    },
  }
}

getMdxNode

getMdxNode(sourceName, context, params) returns an MDXNode.

  • sourceName is the unique ID defined in next-mdx.json
  • context is the context passed to getStaticProps or the slug as a string.
  • params:
{
  components?: MdxRemote.Components
  scope?: Record<string, unknown>
  provider?: MdxRemote.Provider
  mdxOptions?: {
    remarkPlugins?: Pluggable[]
    rehypePlugins?: Pluggable[]
    hastPlugins?: Pluggable[]
    compilers?: Compiler[]
    filepath?: string
  }
}

Example

// pages/blog/[...slug].js
import { getMdxNode } from "next-mdx/server"

export async function getStaticProps(context) {
  const post = await getMdxNode("post", context)

  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: {
      post,
    },
  }
}

getAllMdxNodes

getAllMdxNodes(sourceName, params) returns all MdxNode of the given type/source.

  • sourceName is the unique ID defined in next-mdx.json
  • params:
{
  components?: { name: React.Component },
  scope?: {},
  provider?: { component: React.Component, props: Record<string, unknown> },
  mdxOptions: {
    remarkPlugins: [],
    rehypePlugins: [],
    hastPlugins: [],
    compilers: [],
  }
}

Example

import { getAllMdxNodes } from "next-mdx/server"

export async function getStaticProps() {
  const posts = await getAllMdxNodes("post")

  return {
    props: {
      posts: posts.filter((post) => post.frontMatter.featured),
    },
  }
}

useHydrate

useHydrate(node, params) is used on the client side for hydrating static content.

  • node is the MdxNode object
  • params:
{
  components?: { name: React.Component },
  provider?: { component: React.Component, props: Record<string, unknown> }
}

Example

import { useHydrate } from "next-mdx/client"

export default function PostPage({ post }) {
  const content = useHydrate(post)

  return (
    <div>
      <h1>{post.frontMatter.title}</h1>

      {content}
    </div>
  )
}

getAllNodes vs getAllMdxNodes

Use getAllNodes when you need nodes without the MDX content. It is backed by a cache and is really fast. This is handy when you need a list of nodes (example post teasers) and you're not using the MDX content.

MDX Components

To use components inside MDX files, you need to pass the components to both getMdxNode/getAllMdxNodes and useHydrate.

Example

import { getMdxNode } from "next-mdx/server"
import { useHydrate } from "next-mdx/client"

export function Alert({ text }) {
  return <p>{text}</p>
}

export default function PostPage({ post }) {
  const content = useHydrate(post, {
    components: {
      Alert,
    },
  })

  return (
    <div>
      <h1>{post.frontMatter.title}</h1>

      {content}
    </div>
  )
}

export async function getStaticProps(context) {
  const post = await getMdxNode("post", context, {
    components: {
      Alert,
    },
  })

  return {
    props: {
      post,
    },
  }
}

MDX Options

MDX options can be passed as params to both getMdxNode(sourceName, context, params) and getAllMdxNodes(sourceName, params) where params takes the shape of:

export interface MdxParams {
  components?: MdxRemote.Components
  scope?: Record<string, unknown>
  provider?: MdxRemote.Provider
  mdxOptions?: {
    remarkPlugins?: Pluggable[]
    rehypePlugins?: Pluggable[]
    hastPlugins?: Pluggable[]
    compilers?: Compiler[]
    filepath?: string
  }
}

Relational Data

When retrieving nodes with getMdxNode or getAllMdxNodes, next-mdx will automatically infer relational data from frontMatter keys.

Convention

  1. The frontMatter field name must be the same as the key defined in next-mdx.json
  2. The frontMatter field must be an array of values.

Example

Given the following MDX files.

.
└── content
    ├── categories
    │   └── category-a.mdx
    │   └── category-b.mdx
    └── posts:
        └── example-post.mdx

In example-post you can reference related categories using the following:

---
title: Example Post
category:
  - category-a
---

You can then access the categories as follows:

const post = getMdxNode("post", context)

// post.relationships.category

Plugins

TypeScript

Define your node types as follows:

interface Post extends MdxNode<FrontMatterFields> {}

Example

import { MdxNode } from "next-mdx/server"

interface Category
  extends MdxNode<{
    name: string
  }> {}

interface Post
  extends MdxNode<{
    title: string
    excerpt?: string
    category?: string[]
  }> {
  relationships?: {
    category: Category[]
  }
}

You can then use Post as the return type for getNode, getAllNodes, getMdxNode and getAllMdxNode:

const post = await getMdxNode<Post>("post", context)

const posts = await getAllNodes<Post>("post")

License

Licensed under the MIT license.

next-mdx's People

Contributors

dependabot[bot] avatar ruheni avatar shadcn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

next-mdx's Issues

Add support for next-mdx.config.js

Replace next-mdx.json with next-mdx.config.js.

TBH I can't figure out how to make this work (probably limited to using micribundle?).

If anyone want to take a stab at this, that'd be awesome.

Handling Next localization with next.config.js

Is there a workaround for handling locales? I get 404 when trying to get to the blog with a locale.

http://localhost:3000/blog/post-slug will return the post correctly but http://localhost:3000/es-MX/blog/post-slug will return 404.

I don't mind if it returns the same content, I just want the post to be rendered as the same if the url has the locale.

Add reading time, word count

Fantastic library, I'm really enjoying the simplicity it brings to building an mdx-powered site with relational data. It would be useful to add reading-time and wordCount parameters to the frontmatter. Would you accept a PR for this? Looking at the source, I believe it would be easiest to add these parameters in get-nodes.ts in the getFileData() function.

Multiple slugs for the same document

Would this be possible? I am fine with just using the main slug (i.e. the filename) to reference a document internally but I would like multiple slugs to resolve to the same file.

Render inlineCode and code

is there an option to separate code and inlineCode in the components?
Here's my code:

const components = { h1: ({ children }) => { return <Heading as="h1">{children}</Heading>; } code: ({ children }) => { return <p sx={{ color: "blue", background: 'red' }}>{children}</p>; }, inlineCode: ({ children }) => { return <p sx={{ color: "red", background: 'yellow' }}>{children}</p>; } };

const content = useHydrate(doc, { components: components, });

rendering images in mdx posts

I have tried to publish .mdx posts with images but it doesn't seem to work. I tried inserting this:

<Image src={/images/placeholder.png} alt={image alt text} layout="fill"  />

Any ideas how to post images?
Thanks

Filtering documents by relationship

I have two types of nodes: Post and Snippet. I would like a Post to be composed of multiple snippets (which Snippet it belongs to is defined in the Snipper's front matter), while retaining the ability to refer to a Snippet on its own.

Is it possible to call getAll*Nodes('snippet') from the Post document and only have it return the Snippets that belong to the post?

getMdxNode's context type is incompatible with nextjs's

For this code:

import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async (context) => {
    const post = await getMdxNode('post', context);

    if (!post) {
        return {
            notFound: true,
        };
    }

    return {
        props: {
            post,
        },
    };
};

TypeScript is emitting this warning for L3:

TS2345: Argument of type 'GetStaticPropsContext<ParsedUrlQuery>' is not assignable to parameter of type 'string | GetStaticPropsContext<Dict<string[]>>'.  
Type 'GetStaticPropsContext<ParsedUrlQuery>' is not assignable to type 'GetStaticPropsContext<Dict<string[]>>'.
Type 'ParsedUrlQuery' is not assignable to type 'Dict<string[]>'. 
Index signatures are incompatible.
Type 'string | string[] | undefined' is not assignable to type 'string[] | undefined'.
Type 'string' is not assignable to type 'string[] | undefined'.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.