-
Notifications
You must be signed in to change notification settings - Fork 199
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
Comments
Great point! Will investigate. Would you expect the images become part of the deployment or be uploaded somewhere else? |
I personally thought for my use case abound bundling it with the MDX so it becomes part of the deployment. |
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 Based on the provided image-related instructions of mdx-bundler I've done a quick experiment and got things to work using the 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. 👀 |
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 |
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
I once upon a time used https://github.com/sergioramos/remark-copy-linked-files but usually in running stuff via Ideally if we doing a content layer from local files, then the |
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 {
// ...
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 What do you think? |
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. |
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.
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 |
Just wanted to say +1 and also wondering if the new Next.js |
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! |
I think the issue is that the underlying |
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? |
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
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. 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; |
I've recently been migrating my blog from Gatsby to Next.js, and this issue mentions that 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) |
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 👍 |
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! |
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
|
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.The text was updated successfully, but these errors were encountered: