kentcdodds / mdx-bundler

🦤 Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!
MIT License
1.75k stars 75 forks source link

Image in frontmatter #70

Open jeffreyquan opened 3 years ago

jeffreyquan commented 3 years ago

Hi,

Thanks for this wonderful package ☺️

I was wondering if it's possible to use mdx-bundler to parse relative image file references in frontmatter. It's working perfectly for images in the mdx content itself but the images referenced in the frontmatter aren't written to the output directory.

I would like to use the frontmatter to display a preview of the blog post in card format, including the banner.

Any guidance would be much appreciated!

Relevant code or config

---
title: My first blog post
date: "2021-07-22"
banner: "./images/banner.jpg"
---
const ROOT_PATH = process.cwd();
const PUBLIC_PATH = path.join(ROOT_PATH, "public");

const { code, frontmatter } = await bundleMDX(source, {
    cwd: directory,
    files,
    xdmOptions: (options) => {
      options.remarkPlugins = [
        ...(options.remarkPlugins ?? []),
        remarkMdxImages,
        gfm,
      ];

      return options;
    },
    esbuildOptions: (options) => {
      options.entryPoints = [getFilePath(directory)];
      options.outdir = path.join(PUBLIC_PATH, "images", type, slug);
      options.loader = {
        ...options.loader,
        ".webp": "file",
        ".png": "file",
        ".jpg": "file",
        ".jpeg": "file",
        ".gif": "file",
      };
      options.publicPath = `/images/${type}/${slug}`;
      options.write = true;

      return options;
    },
  });

What you did:

Ran bundleMDX in NextJS to bundle my MDX files.

What happened:

No error messages; the image file referenced in the frontmatter isn't written to the outdir like the images in the content of the mdx file.

Arcath commented 3 years ago

If you have the image in your frontmatter you could use fs yourself to copy it into place and make any changes you need.

You could add a Banner component in your mdx that uses the image so esbuild will handle it like other images, not sure how that would fit in with your component structure.

jeffreyquan commented 3 years ago

Thanks @Arcath. Looks like using fs to copy images into the public directory is my best bet.

If I import a Banner component and use an image as my banner, is there a way during the esbuild process that the file path of the banner image can be outputted so I can store it in my frontmatter and use it to display outside of my blog plage?

deadcoder0904 commented 3 years ago

@jeffreyquan do share it when you're done. I'm thinking of doing the same thing for my blog :)

CanRau commented 2 years ago

One alternative to frontmatter, which works for me, is exporting the image like so

export { default as banner } from "./images/banner.jpg";

then you access it like so

import { getMDXExport } from "mdx-bundler/client";
const { code } = bundleMDX(...)
const { banner } = getMDXExport(code);

Hope this helps, uh also, you could write your whole frontmatter as an exported JS object

CanRau commented 2 years ago

Actually now that I'm using the yaml frontmatter to add JSON-LD to my remix website it'd be really nice to have relative images work in frontmatter directly

An alternative, though a little more "complicated" is:

import coverImage from "./cover.jpg";
import secondImage from "./second-image.jpg";
export const cover = coverImage;

export const jsonld = {
  "@context": "https://schema.org",
  "@type": "NewsArticle",
  headline: "Article headline",
  image: [coverImage, secondImage],
  datePublished: "2015-02-05T08:00:00+08:00",
  dateModified: "2015-02-05T09:20:00+08:00",
  author: [
    {
      "@type": "Person",
      name: "Can Rau",
      url: "https://twitter.com/CanRau"
    },
  ],
};

but this way I have to first import them, export the cover, add them to the image property array etc and also this way Dates aren't automatically handled, so I'd have to define them using Date() or something I guess, which adds more complexity 😳

city17 commented 2 years ago

@jeffreyquan Curious if you found an optimal solution in the end? I'm currently trying to implement the same thing on my blog.

jeffreyquan commented 2 years ago

Thanks for sharing, @CanRau!

@deadcoder0904 and @city17, I haven't implemented an optimal solution yet since I have not been blogging. I have created an images folder in my public directory and stored each image relevant to a blog post within a folder that has the slug of the blog post as the name of the folder and reference this in the frontmatter of the mdx files e.g.

---
title: My first blog post
date: "2021-07-22"
banner:   /images/blog/my-first-blog-post/banner.png
---

Other images referenced in the body of the mdx are copied to the same folder by mdx-bundler

deadcoder0904 commented 2 years ago

i went with this solution but would love an official way to do it from the frontmatter 👍

universse commented 1 year ago

I wrote a remark plugin to handle this. Hope it helps.

function mdxOptions(options, frontmatter) {
  options.remarkPlugins = [
    ...(options.remarkPlugins ?? []),
    [
      remarkMDXImageFromFrontmatter,
      { name: `banner`, from: frontmatter.banner},
    ],
  ]

  options.rehypePlugins = [...(options.rehypePlugins ?? [])]

  return options
}

function remarkMDXImageFromFrontmatter(options = {}) {
  return (ast) => {
    const name = options.name || `banner`

    ast.children.unshift({
      type: `mdxjsEsm`,
      value: `export { default as ${name} } from '${options.from}'`,
      data: {
        estree: {
          type: `Program`,
          body: [
            {
              type: `ExportNamedDeclaration`,
              declaration: null,
              specifiers: [
                {
                  type: `ExportSpecifier`,
                  local: {
                    type: `Identifier`,
                    name: `default`,
                  },
                  exported: {
                    type: `Identifier`,
                    name,
                  },
                },
              ],
              source: {
                type: `Literal`,
                value: options.from,
                raw: `'${options.from}'`,
              },
            },
          ],
          sourceType: `module`,
        },
      },
    })
  }
}

Then get it via

const { banner } = getMDXExport(code);