wooorm / xdm

Just a *really* good MDX compiler. No runtime. With esbuild, Rollup, and webpack plugins
http://wooorm.com/xdm/
MIT License
595 stars 18 forks source link

How to pass data (frontmatter, exports) to layout? #27

Closed kripod closed 3 years ago

kripod commented 3 years ago

Thank you for creating this library!

I would like to know how to access named (non-default) exports of MDX files via webpack (Next.js) from the rendering component. My main use-case is loading metadata, e.g. title for blog posts and then embedding the latter inside a <title> element:

---
title: Hello, world!
---

export { BlogPostLayout as default } from "./BlogPostLayout";

# Hello, world!

This is my first post.

// BlogPostLayout.jsx

export function BlogPost({ meta, children }) {
    console.log({ meta }); // undefined – should be populated with frontmatter data
    return (
        <article>
            {children}
        </article>
    );
}
wooorm commented 3 years ago

Heya! 👋

The frontmatter guide shows several ways to do it. Plus it links to a plugin that does what you want I believe!

kripod commented 3 years ago

I’ve followed that guide but haven’t been successful, unfortunately. I cannot even read a simple named export from BlogPostLayout.jsx, e.g. the title from the file below:

// BlogPost.mdx

export const title = "Hello, world!";

# Hello, world!

This is my first post.
wooorm commented 3 years ago

Then it sounds like something else is going on. This seems next related, or related to your setup. Can you provide more info?

kripod commented 3 years ago

Sure, I’ll set up a reproduction case soon, thank you for helping.

kripod commented 3 years ago

I’ve just set up a minimalistic reproduction case over here.

Following the official Next.js docs, I’ve bootstrapped the application using npx create-next-app. After that, I’ve made a commit with the following changes:

  1. npm install xdm
  2. Add a next.config.js file, as suggested by xdm’s readme. Adding "mdx" to pageExtensions was also necessary, although it isn’t yet documented here.
  3. Add a simplistic BlogPostLayout component with an accompanying hello-world.mdx file
  4. Run the application with npm run dev and then see console outputs on the client and the server when navigating to http://localhost:3000/blog/hello-world.
wooorm commented 3 years ago

Ah. Your question is about how to pass things to the layout. I missed that.

Here’s how I’d solve that:

import { BlogPostLayout } from "../components/BlogPostLayout"

export const title = "Hello, world!"

export default function Layout(props) {
  return <BlogPostLayout {...props} title={title} />
}

...

This makes a lot of sense to me. Should it be different?

kripod commented 3 years ago

Oh I see, thank you!

I’m not sure if it should be any different, but then I cannot use the frontmatter data exported directly, e.g.:

---
title: Hello, world!
---

// Doesn’t get `title` (or `meta.title`)
export { BlogPostLayout as default } from "./BlogPostLayout";

# Hello, world!

This is my first post.
wooorm commented 3 years ago

That’s what remark-mdx-frontmatter does. You can configure it to put everything in a frontmatter variable or so. and then pass it through just like how I proposed passing title through

wooorm commented 3 years ago

I don’t think xdm should do anything here. It’s how JavaScript works:

// example.js:
var someData = 'whatever'

import SomeComponent from './somecomponent.js'

<SomeComponent />

// somecomponent.js:
export default function SomeComponent(props) {
  console.log(props.someData) // undefined
}
kripod commented 3 years ago

I already used remark-mdx-frontmatter, thanks for the suggestion.

Should I pass meta as follows when the name: "meta" option is set with remark-mdx-frontmatter, then?

---
title: Hello, world
---

import { BlogPostLayout } from "../components/BlogPostLayout"

export default function Layout(props) {
  return <BlogPostLayout {...props} meta={meta} />
}

# Hello, world!

…
wooorm commented 3 years ago

Yep! Or spread it in if you want the top-level keys: {...meta}. And maybe invert the two just to prefer given props over meta {...meta} {...props}, if that’s what you want.

eyelidlessness commented 3 years ago

Sorry to bump a closed thread but this seems like at least something which should be called out in the docs as a difference from @mdx-js. The expected behavior is that frontmatter is collected as a local layoutProps which in turn is passed as spread props to the layout/wrapper component.

If @wooorm believes this behavior is out of scope for xdm, I can understand (there are a bunch of other differences) but I do think it should be documented. In scope or not, I spent the last couple days implementing it (some ambiguous-to-me estree corner cases notwithstanding). It’s not quite ready for general use but I wanted to get an idea of whether this belongs in xdm or as a plugin before going much further.


Just to state my perspective on where it belongs: the unified ecosystem is generally very BYO*, but the MDX ecosystem is much more high level/opinionated/batteries-included, and documented as such.

To the extent that becomes fractured it’s a pretty heavy burden both on documentation and end users trying to discover it. Ideally for my usage, xdm‘s incompatibilities with @mdx-js would be limited by technical necessity or with contrib/clear instructions where there’s philosophical/scope differences.

I’m especially sensitive to this because I’m working on a contract where I’ll be handing over maintenance to other devs, and the big elephant in the room for me in this role is whether I can vouch for the maintainability of an xdm integration when there are unknown discrepancies with the base spec.

I also know that’s a lot to ask of any open source maintainer. To the extent level of effort/time investment is a deciding factor, I’d be happy to offer my help.

wooorm commented 3 years ago

as a difference from @mdx-js

What is the difference here? mdx-js/mdx doesn’t handle frontmatter.

eyelidlessness commented 3 years ago

It doesn’t handle frontmatter, but it does pass your exports to your wrapper:

You’ll also notice that layoutProps is created based on your exports and then passed to the wrapper. This allows for the wrapper to use those props automatically for handling things like adding an author bio to the wrapped document.

The relevance of frontmatter is that it typically gets converted to exports in MDX (via plugin), which in turn are passed as props to the component.

wooorm commented 3 years ago

@eyelidlessness https://github.com/mdx-js/mdx/issues/742

eyelidlessness commented 3 years ago

Well that’s news to me, and a little baffling to say the least. This means anyone importing a given MDX module will have to know about its exports and apply them manually to a default wrapper, which is not a great authoring experience.

It hasn’t landed in the current release, it looks like it’s targeted for 2.x. I can understand not wanting to implement something that’s essentially deprecated, but maybe a note in the docs would be helpful for others expecting the current/documented behavior? And I’ll plan on open sourcing my solution as a plugin.

wooorm commented 3 years ago

Well that’s news to me, and a little baffling to say the least

You might raise a new issue there about it. I don’t know exactly what the reason for the change is.

This means anyone importing a given MDX module will have to know about its exports and apply them manually to a default wrapper, which is not a great authoring experience.

How about this?

import * as AllTheThings from './some-mdx-file.mdx'
import {MyFancyLayout} from './layout.jsx'

var {default: MDXContent, ...props} = AllTheThings

<MDXContent {...props} components={{wrapper: MyFancyLayout}} />
eyelidlessness commented 3 years ago

Well that’s news to me, and a little baffling to say the least

You might raise a new issue there about it. I don’t know exactly what the reason for the change is.

Good point, I’ll ask there.

This means anyone importing a given MDX module will have to know about its exports and apply them manually to a default wrapper, which is not a great authoring experience.

How about this?

import * as AllTheThings from './some-mdx-file.mdx'
import {MyFancyLayout} from './layout.jsx'

var {default: MDXContent, ...props} = AllTheThings

<MDXContent {...props} components={{wrapper: MyFancyLayout}} />

🤦‍♂️ Well that’s a heck of a lot simpler than my convoluted plugin. I don’t know why I keep forgetting namespace imports exist. Thanks! I’ll give that a try soon as I’m back at my desk.