Skip to content

Commit

Permalink
[MDX] Support img component prop for optimized images (#8468)
Browse files Browse the repository at this point in the history
Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
  • Loading branch information
bholmesdev and Princesseuh committed Sep 13, 2023
1 parent ecc65ab commit a8d72ce
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 4 deletions.
48 changes: 48 additions & 0 deletions .changeset/cyan-penguins-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@astrojs/mdx': minor
---

Support the `img` component export for optimized images. This allows you to customize how optimized images are styled and rendered.

When rendering an optimized image, Astro will pass the `ImageMetadata` object to your `img` component as the `src` prop. For unoptimized images (i.e. images using URLs or absolute paths), Astro will continue to pass the `src` as a string.

This example handles both cases and applies custom styling:

```astro
---
// src/components/MyImage.astro
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';
type Props = {
src: string | ImageMetadata;
alt: string;
};
const { src, alt } = Astro.props;
---
{
typeof src === 'string' ? (
<img class="custom-styles" src={src} alt={alt} />
) : (
<Image class="custom-styles" {src} {alt} />
)
}
<style>
.custom-styles {
border: 1px solid red;
}
</style>
```

Now, this components can be applied to the `img` component props object or file export:

```md
import MyImage from '../../components/MyImage.astro';

export const components = { img: MyImage };

# My MDX article
```
10 changes: 9 additions & 1 deletion packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Plugin as VitePlugin } from 'vite';
import { getRehypePlugins, getRemarkPlugins, recmaInjectImportMetaEnvPlugin } from './plugins.js';
import type { OptimizeOptions } from './rehype-optimize-static.js';
import { getFileInfo, ignoreStringPlugins, parseFrontmatter } from './utils.js';
import { ASTRO_IMAGE_ELEMENT, ASTRO_IMAGE_IMPORT, USES_ASTRO_IMAGE_FLAG } from './remark-images-to-component.js';

export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | 'rehypePlugins'> & {
extendMarkdownConfig: boolean;
Expand Down Expand Up @@ -194,12 +195,19 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
if (!moduleExports.find(({ n }) => n === 'Content')) {
// If have `export const components`, pass that as props to `Content` as fallback
const hasComponents = moduleExports.find(({ n }) => n === 'components');
const usesAstroImage = moduleExports.find(({n}) => n === USES_ASTRO_IMAGE_FLAG);

let componentsCode = `{ Fragment${hasComponents ? ', ...components' : ''}, ...props.components,`
if (usesAstroImage) {
componentsCode += ` ${JSON.stringify(ASTRO_IMAGE_ELEMENT)}: ${hasComponents ? 'components.img ?? ' : ''} props.components?.img ?? ${ASTRO_IMAGE_IMPORT}`;
}
componentsCode += ' }';

// Make `Content` the default export so we can wrap `MDXContent` and pass in `Fragment`
code = code.replace('export default MDXContent;', '');
code += `\nexport const Content = (props = {}) => MDXContent({
...props,
components: { Fragment${hasComponents ? ', ...components' : ''}, ...props.components },
components: ${componentsCode},
});
export default Content;`;
}
Expand Down
12 changes: 9 additions & 3 deletions packages/integrations/mdx/src/remark-images-to-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { MdxJsxFlowElement, MdxjsEsm } from 'mdast-util-mdx';
import { visit } from 'unist-util-visit';
import { jsToTreeNode } from './utils.js';

export const ASTRO_IMAGE_ELEMENT = 'astro-image';
export const ASTRO_IMAGE_IMPORT = '__AstroImage__';
export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage';

export function remarkImageToComponent() {
return function (tree: any, file: MarkdownVFile) {
if (!file.data.imagePaths) return;
Expand Down Expand Up @@ -48,7 +52,7 @@ export function remarkImageToComponent() {

// Build a component that's equivalent to <Image src={importName} alt={node.alt} title={node.title} />
const componentElement: MdxJsxFlowElement = {
name: '__AstroImage__',
name: ASTRO_IMAGE_ELEMENT,
type: 'mdxJsxFlowElement',
attributes: [
{
Expand Down Expand Up @@ -92,7 +96,9 @@ export function remarkImageToComponent() {
// Add all the import statements to the top of the file for the images
tree.children.unshift(...importsStatements);

// Add an import statement for the Astro Image component, we rename it to avoid conflicts
tree.children.unshift(jsToTreeNode(`import { Image as __AstroImage__ } from "astro:assets";`));
tree.children.unshift(jsToTreeNode(`import { Image as ${ASTRO_IMAGE_IMPORT} } from "astro:assets";`));
// Export `__usesAstroImage` to pick up `astro:assets` usage in the module graph.
// @see the '@astrojs/mdx-postprocess' plugin
tree.children.push(jsToTreeNode(`export const ${USES_ASTRO_IMAGE_FLAG} = true`));
};
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Optimized image:
![Houston](../assets/houston.webp)

Public image:
![Astro logo](/favicon.svg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';
type Props = {
src: string | ImageMetadata;
alt: string;
};
const { src, alt } = Astro.props;
---

{
typeof src === 'string' ? (
<img data-my-image src={src} alt={alt} />
) : (
<Image data-my-image {src} {alt} />
)
}

<style>
[data-my-image] {
border: 1px solid red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Optimized image:
![Houston](../../assets/houston.webp)

Public image:
![Astro logo](/favicon.svg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
import { getEntry } from 'astro:content';
import MyImage from 'src/components/MyImage.astro';
const entry = await getEntry('blog', 'entry');
const { Content } = await entry.render();
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Renderer</title>
</head>
<body>
<Content components={{ img: MyImage }} />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import MDX from '../components/Component.mdx';
import MyImage from 'src/components/MyImage.astro';
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Renderer</title>
</head>
<body>
<MDX components={{ img: MyImage }} />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import MyImage from '../components/MyImage.astro';

export const components = { img: MyImage };

Optimized image:
![Houston](../assets/houston.webp)

Public image:
![Astro logo](/favicon.svg)
23 changes: 23 additions & 0 deletions packages/integrations/mdx/test/mdx-images.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { expect } from 'chai';
import { parseHTML } from 'linkedom';
import { loadFixture } from '../../../astro/test/test-utils.js';

const imageTestRoutes = ['with-components', 'esm-import', 'content-collection']

describe('MDX Page', () => {
let devServer;
let fixture;
Expand Down Expand Up @@ -36,5 +38,26 @@ describe('MDX Page', () => {
// Image with spaces in the path
expect(imgs.item(3).src.startsWith('/_image')).to.be.true;
});

for (const route of imageTestRoutes) {
it(`supports img component - ${route}`, async () => {
const res = await fixture.fetch(`/${route}`);
expect(res.status).to.equal(200);

const html = await res.text();
const { document } = parseHTML(html);

const imgs = document.getElementsByTagName('img');
expect(imgs.length).to.equal(2);

const assetsImg = imgs.item(0);
expect(assetsImg.src.startsWith('/_image')).to.be.true;
expect(assetsImg.hasAttribute('data-my-image')).to.be.true;

const publicImg = imgs.item(1);
expect(publicImg.src).to.equal('/favicon.svg');
expect(publicImg.hasAttribute('data-my-image')).to.be.true;
});
}
});
});

0 comments on commit a8d72ce

Please sign in to comment.