Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for importing images #11

Open
henniaufmrenni opened this issue Oct 27, 2021 · 18 comments
Open

Add support for importing images #11

henniaufmrenni opened this issue Oct 27, 2021 · 18 comments
Labels
feature New feature or request topic: images
Milestone

Comments

@henniaufmrenni
Copy link

One nice feat about mdx-bundler is that it allows for image bundling through import statements.
https://github.com/kentcdodds/mdx-bundler#image-bundling
Implementing this would be especially helpful for the very limited next/image, because there wouldn't be the need for width/size specification and it would provide a blur hash automatically.

@schickling
Copy link
Collaborator

Great point! Will investigate. Would you expect the images become part of the deployment or be uploaded somewhere else?

@henniaufmrenni
Copy link
Author

I personally thought for my use case abound bundling it with the MDX so it becomes part of the deployment.

@schickling
Copy link
Collaborator

I've done a quick investigation and I think there is a really great opportunity for Contentlayer to make working with images as part of your content much easier - especially in Next.js applications where for the <Image /> component you need to provide the size dimensions.

Based on the provided image-related instructions of mdx-bundler I've done a quick experiment and got things to work using the dataurl "image embedding". For most cases this is probably not a great solution but it's already something that could be done in the meanwhile by users waiting for this feature.

image

I'm really excited about diving into this feature but it's just a matter about finding enough time for it as it's quite a serious time investment. 👀

@EthanStandel
Copy link

For my site, I have several products each with an image set. So I need access to a list for an image-carousel. I don't feel like my use-case is an edge-case.

I would really like to see the ability to just get a manifest for all images in my /public directory. So like import { allImages } from '.contentlayer/data'; would give you just the paths for images from /public. Right now, I'm just using a prebuild script to generate imageManifest.json files. This works but feels like the kind of hackery that contentlayer could help me get out of doing for an individual project.

@browniefed
Copy link
Contributor

I ended up using a computed field, parsing the doc, copying to public, and then having a map. Then wrapped my rendering document in a context provider, Then for my img component replace it with a custom image component that utilizes next/image and the size analyzed at build time.

{ './local.jpg': { size: {width:100,height:100}, url: '/location-in-public-[hash].jpg } }

I once upon a time used https://github.com/sergioramos/remark-copy-linked-files but usually in running stuff via mdx-bundler the VFile doesn't have a file path.

Ideally if we doing a content layer from local files, then the cwd would be set correctly and theoretically contentlayer could avoid in some cases complex image management and leave it to remark plugins. But just a thought

@sisp
Copy link

sisp commented Apr 17, 2022

An additional thought on this topic:

I'd like to be able to import SVGs in a Next.js app via Contentlayer and embed them instead of rendering an <img/> with a data URL in order to apply styles (mainly fill color) to the SVGs. I've been thinking whether it's possible to leverage SVGR to convert an SVG to a React component in Contentlayer. Perhaps a field definition could look like this:

{
 // ...
  fields: {
    // ...
    icon: { type: "svg", required: true },
  }
}

and the data received in the Next.js app would be a React component of the SVG that can be rendered like any other component, the only difference is that it isn't imported via import ... from ... but provided by Contentlayer.

What do you think?

@schickling
Copy link
Collaborator

Those are all super interesting ideas. I'm hoping to get to working this feature in the near future but most likely still a few months away.

Saeris added a commit to Saeris/contentlayer that referenced this issue Apr 26, 2022
Implementation for the request in contentlayerdev#192 that also partially solves for contentlayerdev#11.

In `@contentlayer/core` I added a new `data` field to the  options argument of `markdownToHtml` and `bundleMDX` which can be used to pass arbitrary data to the resulting document's `vFile` `data` property.

Not knowing how you want to structure utility functions for this library, in the initial implementation I've inlined the `addRawDocumentMeta` remark plugin used to append the vFile inside of both `markdownToHtml` and `bundleMDX`. Please advise me if you'd like this extracted out to somewhere else instead.

In this PR I've only updated the `mapping.ts` file for the `source-files` package, as I'm unsure whether other sources like `source-contentful` expose the same `RawDocumentData` that the filesystem source does. Other sources can pass whatever document metadata is pertinent to them to the markdown processors using this addition to their APIs.
@michaeloliverx
Copy link

Being able to import images and having them included in the bundle as assets is something I would like. I currently achieve this in Next.js using @mdx-js/loader and remark-mdx-images and it allows me to use the Next.js <Image/> component easily.

@adrianmg
Copy link
Contributor

adrianmg commented Jul 9, 2022

Just wanted to say +1 and also wondering if the new Next.js next/future/image will make any of this work easier.

@browniefed
Copy link
Contributor

Will this support an array of images? In some cases if I wanted to add a carousel of images surrounding a blog post. I cannot seem to tell if the current proposal would allow for that.

@schickling
Copy link
Collaborator

Will this support an array of images? In some cases if I wanted to add a carousel of images surrounding a blog post. I cannot seem to tell if the current proposal would allow for that.

Good question. Probably not yet supported right now but will be supported in the future!

@inflation
Copy link

I think the issue is that the underlying mdx-bundler is using esbuild, which doesn't know the webpack config from next/image. So, you can't simply put import img from "sample.png" in your MDX file and call it a day. @mdx-js/loader works fine here because of that.

@AnweshGangula
Copy link

AnweshGangula commented Jun 19, 2023

Did Contentlayer already implement this feature? I see the release notes for 0.2.7 mention relative url for cover_image in markdown frontmatter. Is this not the same request in terms of development? So is it possible to import making use of this?

@mmazzarolo
Copy link

For what is worth, I recently migrated my blog from Gatsby to Next.js 13 + Contentlayer and I used a couple of small custom remark + rehype plugins to allow referencing relative assets from MDX files and to automatically add the width and height attributes. I prefer this setup compared to importing images using a static import.
I documented them here:

They work on both markdown and MDX syntax (so for custom images). I'm pretty sure this is a common pattern to handle these cases, but I wanted to share them here as well since they weren't mentioned.
By the way, excellent work with Contantlayer! I love it!

I'll copy/paste the snippets here as well, hope they can help!

/**
 * rehype-image-size.js
 *
 * Requires:
 * - npm i image-size unist-util-visit
 */
import getImageSize from "image-size";
import { visit } from "unist-util-visit";
 
/**
 * Analyze local MDX images and add `width` and `height` attributes to the
 * generated `img` elements.
 * Supports both markdown-style images and MDX <Image /> components.
 * @param {string} options.root - The root path when reading the image file.
 */
export const rehypeImageSize = (options) => {
  return (tree) => {
    // This matches all images that use the markdown standard format ![label](path).
    visit(tree, { type: "element", tagName: "img" }, (node) => {
      if (node.properties.width || node.properties.height) {
        return;
      }
      const imagePath = `${options?.root ?? ""}${node.properties.src}`;
      const imageSize = getImageSize(imagePath);
      node.properties.width = imageSize.width;
      node.properties.height = imageSize.height;
    });
    // This matches all MDX' <Image /> components.
    // Feel free to update it if you're using a different component name.
    visit(tree, { type: "mdxJsxFlowElement", name: "Image" }, (node) => {
      const srcAttr = node.attributes?.find((attr) => attr.name === "src");
      const imagePath = `${options?.root ?? ""}${srcAttr.value}`;
      const imageSize = getImageSize(imagePath);
      const widthAttr = node.attributes?.find((attr) => attr.name === "width");
      const heightAttr = node.attributes?.find((attr) => attr.name === "height");
      if (widthAttr || heightAttr) {
        // If `width` or `height` have already been set explicitly we
        // don't want to override them.
        return;
      }
      node.attributes.push({
        type: "mdxJsxAttribute",
        name: "width",
        value: imageSize.width,
      });
      node.attributes.push({
        type: "mdxJsxAttribute",
        name: "height",
        value: imageSize.height,
      });
    });
  };
};
 
export default rehypeImageSize;
/**
 * remark-assets-src-redirect.js
 *
 * Requires:
 * - npm i image-size unist-util-visit
 */
import { visit } from "unist-util-visit";
 
/**
 * Analyzes local markdown/MDX images & videos and rewrites their `src`.
 * Supports both markdown-style images, MDX <Image /> components, and `source`
 * elements. Can be easily adapted to support other sources too.
 * @param {string} options.root - The root path when reading the image file.
 */
const remarkSourceRedirect = (options) => {
  return (tree, file) => {
    // You need to grab a reference of your post's slug.
    // I'm using Contentlayer (https://www.contentlayer.dev/), which makes it
    // available under `file.data`.But if you're using something different, you
    // should be able to access it under `file.path`, or pass it as a parameter
    // the the plugin `options`.
    const slug = file.data.rawDocumentData.flattenedPath.replace("blog/", "");
    // This matches all images that use the markdown standard format ![label](path).
    visit(tree, "paragraph", (node) => {
      const image = node.children.find((child) => child.type === "image");
      if (image) {
        image.url = `blog-assets/${slug}/${image.url}`);
      }
    });
    // This matches all MDX' <Image /> components & source elements that I'm
    // using within a custom <Video /> component.
    // Feel free to update it if you're using a different component name.
    visit(tree, "mdxJsxFlowElement", (node) => {
      if (node.name === "Image" || node.name === 'source') {
        const srcAttr = node.attributes.find((attribute) => attribute.name === "src");
        srcAttr.value = `blog-assets/${slug}/${srcAttr.value}`)
      }
    });
  };
};
 
export default remarkSourceRedirect;

@syfxlin
Copy link

syfxlin commented Aug 1, 2023

I've recently been migrating my blog from Gatsby to Next.js, and this issue mentions that remark-mdx-images can automatically add an import statement to an image and work with an esbuild loader to process the image. We can build a custom esbuild loader that reads the image size and copies it to the .next/static/media folder, and changes the return value to the Next.js format. This way you can achieve almost the same effect as Next Image.

I haven't uploaded the full code to GitHub yet because I haven't finished migrating my blog yet, so I'll paste some of the code snippets here, hopefully they'll help.

mdx.ts:

import fs from "fs-extra";
import path from "path";
import hasha from "hasha";
import imageSize from "image-size";
import remarkMdxImages from "remark-mdx-images";
import { MDXOptions } from "@contentlayer/core";
import { NextConfig } from "next";
import { Configuration } from "webpack";

// temp media directory
const cache = path.resolve(".next", "cache", "esbuild", "media");
// next media directory
const media = path.resolve(".next", "static", "media");

export const image = async (image: string) => {
  const info = path.parse(image);
  const size = imageSize(image);
  const hash = hasha(image, { algorithm: "md5" }).substring(0, 8);
  const name = path.format({ name: `${info.name}.${hash}`, ext: info.ext });

  // copy image to temp media directory
  if (process.env.NODE_ENV !== "development") {
    await fs.ensureDir(cache);
    await fs.copyFile(image, path.join(cache, name));
  } else {
    await fs.ensureDir(media);
    await fs.copyFile(image, path.join(media, name));
  }

  return {
    src: `/_next/static/media/${name}`,
    blurDataURL: `/_next/image?w=8&q=70&url=${encodeURIComponent(`/_next/static/media/${name}`)}`,
    width: size.width,
    height: size.height,
    blurWidth: size.width && size.height ? 8 : undefined,
    blurHeight: size.width && size.height ? Math.round((size.height / size.width) * 8) : undefined,
  };
};

export const mdxImage = (options?: MDXOptions): MDXOptions => {
  return {
    ...options,
    remarkPlugins: [...(options?.remarkPlugins ?? []), remarkMdxImages],
    esbuildOptions: (esbuild, matter) => {
      esbuild.plugins = esbuild.plugins ?? [];
      esbuild.plugins.push({
        name: "mdx-image",
        setup: async (build) => {
          build.onLoad({ filter: /\.(jpg|jpeg|png|avif)$/i }, async (args) => {
            const data = await image(args.path);
            return {
              loader: "json",
              contents: JSON.stringify(data),
            };
          });
        },
      });

      if (typeof options?.esbuildOptions === "function") {
        return options.esbuildOptions(esbuild, matter);
      } else {
        return esbuild;
      }
    },
  };
};

export const withMdxImage = (config: Partial<NextConfig>): Partial<NextConfig> => {
  return {
    ...config,
    webpack(webpack: Configuration, options: any) {
      webpack.plugins = webpack.plugins ?? [];
      webpack.plugins.push((compiler) => {
        compiler.hooks.afterCompile.tapPromise("MdxImageWebpackPlugin", async () => {
          if (process.env.NODE_ENV !== "development") {
            // ensure media dir
            await fs.ensureDir(media);
            // copy image to next media directory
            await fs.copy(cache, media, { overwrite: true });
          }
        });
      });

      if (typeof config.webpack === "function") {
        return config.webpack(webpack, options);
      } else {
        return webpack;
      }
    },
  };
};

contentlayer.config.ts:

import { makeSource } from "@contentlayer/source-files";

export default makeSource({
  contentDirPath: "./content",
  documentTypes: [...],
  mdx: mdxImage(),
});

next.config.mjs:

import { withMdxImage } from "./utils/mdx";
import { withContentlayer } from "next-contentlayer";

export default withMdxImage(withContentlayer(config));

page.ts:

import Image from "next/image";

const components: MDXComponents = {
  img: (props) => {
    return <Image src={props.src as any} alt={props.alt as any} placeholder="blur" />;
  }
};

const MDX = useMDXComponent(page.body.code);

<MDX components={components} />

post.mdx:

![image](images/abc.jpg)

@rafaell-lycan
Copy link

Hey y'all 👋

Do you have any idea when this will be supported?

I just started using it migrating from Jekyll + Netlify CMS and I'm loving it!

Anywho, great work on ContentLayer 👍

@abheist
Copy link

abheist commented Nov 7, 2023

Did Contentlayer already implement this feature? I see the release notes for 0.2.7 mention relative url for cover_image in markdown frontmatter. Is this not the same request in terms of development? So is it possible to import making use of this?

I think this is working fine, just not providing the correct path of the image. For it I added additional computed field by providing the correct path and getting other things like height and width from type image in field. and this is for the main image defined in frontmatter.

 imagePath: {
    type: 'string' as const,
    resolve: (doc: any) => {
      return doc.image.filePath.split('public')[1]
    },
  }

Not sure, I'll move forward with this approach as I'm more into image compression and it seems more like a hack.

though it might help anyone here!

@zce
Copy link

zce commented Nov 21, 2023

I was also troubled by this issue and gave up on the Contentlayer.

Maybe try my latest new project: https://github.com/zce/velite

P.S. Velite is inspired by Contentlayer, based on Zod and Unified

here is a example

import { defineConfig, s } from 'velite'

export default defineConfig({
  collections: {
    posts: {
      name: 'Post',
      pattern: 'posts/**/*.md',
      schema: s
        .object({
          title: s.string().max(99),
          slug: s.slug('post'),
          date: s.isodate(),
          cover: s.image().optional(),
          metadata: s.metadata(),
          excerpt: s.excerpt(),
          content: s.markdown()
        })
        .transform(data => ({ ...data, permalink: `/blog/${data.slug}` }))
    },
    others: {
      // ...
    }
  }
})

for more example: https://stackblitz.com/edit/velite-nextjs

BTW. Velite docs is not yet complete, but the functionality is mostly stable, although there is still a possibility of significant changes being made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request topic: images
Projects
None yet
Development

No branches or pull requests