From b827ccbd0ed903d357d84dc8c93a2c2a3845eed8 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Wed, 12 Feb 2025 00:30:11 -0500 Subject: [PATCH 01/17] wip: partially working When in regular .md files, the images show up as expected. However, in content md files, they still misbehave. Also, it seems the responsive attributes are only added by the component, not the image transformation that's done later, which I find a bit weird; there's other responsive stuff being done tho?? So maybe I need to fix that while I'm here too. --- .../astro/src/vite-plugin-markdown/images.ts | 29 +++- .../astro/src/vite-plugin-markdown/index.ts | 17 +- .../mdx/src/rehype-images-to-component.ts | 151 ++++++++++-------- packages/markdown/remark/src/index.ts | 3 +- packages/markdown/remark/src/rehype-images.ts | 17 +- .../remark/src/remark-collect-images.ts | 31 ++-- packages/markdown/remark/src/types.ts | 6 +- 7 files changed, 161 insertions(+), 93 deletions(-) diff --git a/packages/astro/src/vite-plugin-markdown/images.ts b/packages/astro/src/vite-plugin-markdown/images.ts index d0ed625358ff..b99d1af233b2 100644 --- a/packages/astro/src/vite-plugin-markdown/images.ts +++ b/packages/astro/src/vite-plugin-markdown/images.ts @@ -1,15 +1,19 @@ export type MarkdownImagePath = { raw: string; safeName: string }; -export function getMarkdownCodeForImages(imagePaths: MarkdownImagePath[], html: string) { +export function getMarkdownCodeForImages( + localImagePaths: MarkdownImagePath[], + remoteImagePaths: string[], + html: string, +) { return ` import { getImage } from "astro:assets"; - ${imagePaths + ${localImagePaths .map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`) .join('\n')} const images = async function(html) { const imageSources = {}; - ${imagePaths + ${localImagePaths .map((entry) => { const rawUrl = JSON.stringify(entry.raw); return `{ @@ -29,6 +33,25 @@ export function getMarkdownCodeForImages(imagePaths: MarkdownImagePath[], html: }`; }) .join('\n')} + ${remoteImagePaths + .map((raw) => { + const rawUrl = JSON.stringify(raw); + return `{ + const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl.replace( + /[.*+?^${}()|[\]\\]/g, + '\\\\$&', + )} + '[^"]*)"', 'g'); + let match; + let occurrenceCounter = 0; + while ((match = regex.exec(html)) !== null) { + const matchKey = ${rawUrl} + '_' + occurrenceCounter; + const props = JSON.parse(match[1].replace(/"/g, '"')); + imageSources[matchKey] = await getImage(props); + occurrenceCounter++; + } + }`; + }) + .join('\n')} return imageSources; }; diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 8876250f9765..e5c4365c7b47 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -75,16 +75,21 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug } let html = renderResult.code; - const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata; + const { + headings, + localImagePaths: rawLocalImagePaths, + remoteImagePaths, + frontmatter, + } = renderResult.metadata; // Add default charset for markdown pages const isMarkdownPage = isPage(fileURL, settings); const charset = isMarkdownPage ? '' : ''; // Resolve all the extracted images from the content - const imagePaths: MarkdownImagePath[] = []; - for (const imagePath of rawImagePaths) { - imagePaths.push({ + const localImagePaths: MarkdownImagePath[] = []; + for (const imagePath of rawLocalImagePaths) { + localImagePaths.push({ raw: imagePath, safeName: shorthash(imagePath), }); @@ -108,8 +113,8 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug ${ // Only include the code relevant to `astro:assets` if there's images in the file - imagePaths.length > 0 - ? getMarkdownCodeForImages(imagePaths, html) + localImagePaths.length > 0 || remoteImagePaths.length > 0 + ? getMarkdownCodeForImages(localImagePaths, remoteImagePaths, html) : `const html = () => ${JSON.stringify(html)};` } diff --git a/packages/integrations/mdx/src/rehype-images-to-component.ts b/packages/integrations/mdx/src/rehype-images-to-component.ts index c903ae511b47..d6e5308c4bbf 100644 --- a/packages/integrations/mdx/src/rehype-images-to-component.ts +++ b/packages/integrations/mdx/src/rehype-images-to-component.ts @@ -73,84 +73,105 @@ function getImageComponentAttributes(props: Properties): MdxJsxAttribute[] { export function rehypeImageToComponent() { return function (tree: Root, file: VFile) { - if (!file.data.astro?.imagePaths?.length) return; + if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length) + return; const importsStatements: MdxjsEsm[] = []; const importedImages = new Map(); visit(tree, 'element', (node, index, parent) => { - if (!file.data.astro?.imagePaths?.length || node.tagName !== 'img' || !node.properties.src) - return; + if (node.tagName !== 'img' || !node.properties.src) return; const src = decodeURI(String(node.properties.src)); - if (!file.data.astro.imagePaths?.includes(src)) return; - - let importName = importedImages.get(src); - - if (!importName) { - importName = `__${importedImages.size}_${src.replace(/\W/g, '_')}__`; - - importsStatements.push({ - type: 'mdxjsEsm', - value: '', - data: { - estree: { - type: 'Program', - sourceType: 'module', - body: [ - { - type: 'ImportDeclaration', - source: { - type: 'Literal', - value: src, - raw: JSON.stringify(src), - }, - specifiers: [ - { - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: importName }, + const isLocalImage = file.data.astro?.localImagePaths?.includes(src); + const isRemoteImage = file.data.astro?.remoteImagePaths?.includes(src); + + let element: MdxJsxFlowElementHast; + if (isLocalImage) { + let importName = importedImages.get(src); + + if (!importName) { + importName = `__${importedImages.size}_${src.replace(/\W/g, '_')}__`; + + importsStatements.push({ + type: 'mdxjsEsm', + value: '', + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ImportDeclaration', + source: { + type: 'Literal', + value: src, + raw: JSON.stringify(src), }, - ], - }, - ], + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: importName }, + }, + ], + }, + ], + }, }, - }, - }); - importedImages.set(src, importName); - } - - // Build a component that's equivalent to - const componentElement: MdxJsxFlowElementHast = { - name: ASTRO_IMAGE_ELEMENT, - type: 'mdxJsxFlowElement', - attributes: [ - ...getImageComponentAttributes(node.properties), - { - name: 'src', - type: 'mdxJsxAttribute', - value: { - type: 'mdxJsxAttributeValueExpression', - value: importName, - data: { - estree: { - type: 'Program', - sourceType: 'module', - comments: [], - body: [ - { - type: 'ExpressionStatement', - expression: { type: 'Identifier', name: importName }, - }, - ], + }); + importedImages.set(src, importName); + } + + // Build a component that's equivalent to + element = { + name: ASTRO_IMAGE_ELEMENT, + type: 'mdxJsxFlowElement', + attributes: [ + ...getImageComponentAttributes(node.properties), + { + name: 'src', + type: 'mdxJsxAttribute', + value: { + type: 'mdxJsxAttributeValueExpression', + value: importName, + data: { + estree: { + type: 'Program', + sourceType: 'module', + comments: [], + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: importName }, + }, + ], + }, }, }, }, - }, - ], - children: [], - }; + ], + children: [], + }; + } else if (isRemoteImage) { + // Build a component that's equivalent to + element = { + name: ASTRO_IMAGE_ELEMENT, + type: 'mdxJsxFlowElement', + attributes: [ + ...getImageComponentAttributes(node.properties), + { + name: 'src', + type: 'mdxJsxAttribute', + value: src, + }, + ], + children: [], + }; + } else { + return; + } - parent!.children.splice(index!, 1, componentElement); + parent!.children.splice(index!, 1, element); }); // Add all the import statements to the top of the file for the images diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index de13523fe13c..53cbfc3441b3 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -152,7 +152,8 @@ export async function createMarkdownProcessor( code: String(result.value), metadata: { headings: result.data.astro?.headings ?? [], - imagePaths: result.data.astro?.imagePaths ?? [], + localImagePaths: result.data.astro?.localImagePaths ?? [], + remoteImagePaths: result.data.astro?.remoteImagePaths ?? [], frontmatter: result.data.astro?.frontmatter ?? {}, }, }; diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index 11d33df9c15b..2a1476441450 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -13,7 +13,7 @@ export function rehypeImages() { if (node.properties?.src) { node.properties.src = decodeURI(node.properties.src); - if (file.data.astro?.imagePaths?.includes(node.properties.src)) { + if (file.data.astro?.localImagePaths?.includes(node.properties.src)) { const { ...props } = node.properties; // Initialize or increment occurrence count for this image @@ -22,6 +22,21 @@ export function rehypeImages() { node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...props, index }); + Object.keys(props).forEach((prop) => { + delete node.properties[prop]; + }); + } else if (file.data.astro?.remoteImagePaths?.includes(node.properties.src)) { + const { ...props } = node.properties; + + const index = imageOccurrenceMap.get(node.properties.src) || 0; + imageOccurrenceMap.set(node.properties.src, index + 1); + + node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ + inferSize: 'width' in props && 'height' in props ? undefined : true, + ...props, + index, + }); + Object.keys(props).forEach((prop) => { delete node.properties[prop]; }); diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts index f09f1c580a3f..8f9b1c1ac483 100644 --- a/packages/markdown/remark/src/remark-collect-images.ts +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -1,37 +1,38 @@ -import type { Image, ImageReference } from 'mdast'; +import type { Root } from 'mdast'; import { definitions } from 'mdast-util-definitions'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; export function remarkCollectImages() { - return function (tree: any, vfile: VFile) { + return function (tree: Root, vfile: VFile) { if (typeof vfile?.path !== 'string') return; const definition = definitions(tree); - const imagePaths = new Set(); - visit(tree, ['image', 'imageReference'], (node: Image | ImageReference) => { + const localImagePaths = new Set(); + const remoteImagePaths = new Set(); + visit(tree, (node) => { + let url: string | undefined; if (node.type === 'image') { - if (shouldOptimizeImage(node.url)) imagePaths.add(decodeURI(node.url)); - } - if (node.type === 'imageReference') { + url = decodeURI(node.url); + } else if (node.type === 'imageReference') { const imageDefinition = definition(node.identifier); if (imageDefinition) { - if (shouldOptimizeImage(imageDefinition.url)) - imagePaths.add(decodeURI(imageDefinition.url)); + url = decodeURI(imageDefinition.url); } } + + if (!url) return; + else if (isValidUrl(url)) remoteImagePaths.add(url); + // Only optimize local images, not paths to `/public` + else if (!url.startsWith("/")) localImagePaths.add(url); }); vfile.data.astro ??= {}; - vfile.data.astro.imagePaths = Array.from(imagePaths); + vfile.data.astro.localImagePaths = Array.from(localImagePaths); + vfile.data.astro.remoteImagePaths = Array.from(remoteImagePaths); }; } -function shouldOptimizeImage(src: string) { - // Optimize anything that is NOT external or an absolute path to `public/` - return !isValidUrl(src) && !src.startsWith('/'); -} - function isValidUrl(str: string): boolean { try { new URL(str); diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index e6a9d362bc06..af6709ecd698 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -11,7 +11,8 @@ declare module 'vfile' { interface DataMap { astro: { headings?: MarkdownHeading[]; - imagePaths?: string[]; + localImagePaths?: string[]; + remoteImagePaths?: string[]; frontmatter?: Record; }; } @@ -67,7 +68,8 @@ export interface MarkdownProcessorRenderResult { code: string; metadata: { headings: MarkdownHeading[]; - imagePaths: string[]; + localImagePaths: string[]; + remoteImagePaths: string[]; frontmatter: Record; }; } From c3feda42cc4f0b83717d3747ea6ea30d391135f9 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:28:50 -0500 Subject: [PATCH 02/17] works i think!! --- packages/astro/src/content/runtime.ts | 26 +++++++++++++++---- .../content-entry-type.ts | 5 +++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 019a4c8b12cf..25b2113a913e 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -414,13 +414,20 @@ async function updateImageReferencesInBody(html: string, fileName: string) { for (const [_full, imagePath] of html.matchAll(CONTENT_LAYER_IMAGE_REGEX)) { try { const decodedImagePath = JSON.parse(imagePath.replaceAll('"', '"')); - const id = imageSrcToImportId(decodedImagePath.src, fileName); - const imported = imageAssetMap.get(id); - if (!id || imageObjects.has(id) || !imported) { - continue; + let image: GetImageResult; + if (validURL(decodedImagePath.src)) { + // Remote image, pass through without resolving import + image = await getImage(decodedImagePath); + } else { + const id = imageSrcToImportId(decodedImagePath.src, fileName); + + const imported = imageAssetMap.get(id); + if (!id || imageObjects.has(id) || !imported) { + continue; + } + image = await getImage({ ...decodedImagePath, src: imported }); } - const image: GetImageResult = await getImage({ ...decodedImagePath, src: imported }); imageObjects.set(imagePath, image); } catch { throw new Error(`Failed to parse image reference: ${imagePath}`); @@ -446,6 +453,15 @@ async function updateImageReferencesInBody(html: string, fileName: string) { }); } +function validURL(src: string): boolean { + try { + new URL(src); + return true; + } catch { + return false; + } +} + function updateImageReferencesInData>( data: T, fileName?: string, diff --git a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts index 6f248853f51e..28804563b03b 100644 --- a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts +++ b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts @@ -28,7 +28,10 @@ export const markdownContentEntryType: ContentEntryType = { }); return { html: result.code, - metadata: result.metadata, + metadata: { + ...result.metadata, + imagePaths: result.metadata.localImagePaths.concat(result.metadata.remoteImagePaths), + }, }; }; }, From 63b98d765747be3d36437945e3c7b65f368006b2 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:49:40 -0500 Subject: [PATCH 03/17] changeset --- .changeset/thick-jeans-trade.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/thick-jeans-trade.md diff --git a/.changeset/thick-jeans-trade.md b/.changeset/thick-jeans-trade.md new file mode 100644 index 000000000000..b209a5d7c63f --- /dev/null +++ b/.changeset/thick-jeans-trade.md @@ -0,0 +1,9 @@ +--- +'@astrojs/mdx': minor +'@astrojs/markdown-remark': minor +'astro': minor +--- + +Optimize remote images in Markdown files + +Previously, Astro only allowed local images to be optimized when included using `![]()` syntax in plain Markdown files. This was because, when the image service was first introduced, it only was able to optimize those images. Now, however, Astro's image service can optimize remote images as well. So, we can add support for this now! \ No newline at end of file From 3b10d82f852389b09484616ee2fc7fdabb610a7b Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Sat, 15 Feb 2025 13:14:53 -0500 Subject: [PATCH 04/17] fix failing test --- .../remark/test/remark-collect-images.test.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/markdown/remark/test/remark-collect-images.test.js b/packages/markdown/remark/test/remark-collect-images.test.js index 669bee595fa5..3eb3cd04bd55 100644 --- a/packages/markdown/remark/test/remark-collect-images.test.js +++ b/packages/markdown/remark/test/remark-collect-images.test.js @@ -10,33 +10,35 @@ describe('collect images', async () => { }); it('should collect inline image paths', async () => { - const markdown = `Hello ![inline image url](./img.png)`; + const markdown = `Hello ![inline image url](./img.png) ![inline remote image url](https://example.com/example.png)`; const fileURL = 'file.md'; const { code, - metadata: { imagePaths }, + metadata: { localImagePaths, remoteImagePaths }, } = await processor.render(markdown, { fileURL }); assert.equal( code, - '

Hello

', + '

Hello

', ); - assert.deepEqual(imagePaths, ['./img.png']); + assert.deepEqual(localImagePaths, ['./img.png']); + assert.deepEqual(remoteImagePaths, ['https://example.com/example.png']); }); it('should add image paths from definition', async () => { - const markdown = `Hello ![image ref][img-ref]\n\n[img-ref]: ./img.webp`; + const markdown = `Hello ![image ref][img-ref] ![remote image ref][remote-img-ref]\n\n[img-ref]: ./img.webp\n[remote-img-ref]: https://example.com/example.jpg`; const fileURL = 'file.md'; const { code, metadata } = await processor.render(markdown, { fileURL }); assert.equal( code, - '

Hello

', + '

Hello

', ); - assert.deepEqual(metadata.imagePaths, ['./img.webp']); + assert.deepEqual(metadata.localImagePaths, ['./img.webp']); + assert.deepEqual(metadata.remoteImagePaths, ['https://example.com/example.jpg']); }); }); From 400a14f1ece1e14c1899e1829a4e4532039ab288 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Sun, 16 Feb 2025 10:44:11 -0500 Subject: [PATCH 05/17] fix failing test 2 This time the fail was becase we were transforming too many remote images. We should inject allowedRemoteDomains into the remark plugin so we only collect the allowed remote images in the first place. --- .../content-entry-type.ts | 5 ++- .../astro/src/vite-plugin-markdown/index.ts | 5 ++- packages/markdown/remark/src/index.ts | 4 +- .../remark/src/remark-collect-images.ts | 28 +++++++------ .../remark/test/remark-collect-images.test.js | 40 +++++++++++++++++-- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts index 28804563b03b..c7018b623668 100644 --- a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts +++ b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts @@ -18,7 +18,10 @@ export const markdownContentEntryType: ContentEntryType = { handlePropagation: true, async getRenderFunction(config) { - const processor = await createMarkdownProcessor(config.markdown); + const processor = await createMarkdownProcessor({ + allowedRemoteDomains: config.image.domains, + ...config.markdown, + }); return async function renderToString(entry) { // Process markdown even if it's empty as remark/rehype plugins may add content or frontmatter dynamically const result = await processor.render(entry.body ?? '', { diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index e5c4365c7b47..5d5c622f14a6 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -60,7 +60,10 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug // Lazily initialize the Markdown processor if (!processor) { - processor = createMarkdownProcessor(settings.config.markdown); + processor = createMarkdownProcessor({ + allowedRemoteDomains: settings.config.image.domains, + ...settings.config.markdown, + }); } const renderResult = await (await processor).render(raw.content, { diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 53cbfc3441b3..3ea2f88d14d2 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -59,7 +59,7 @@ const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK); * Create a markdown preprocessor to render multiple markdown files */ export async function createMarkdownProcessor( - opts?: AstroMarkdownOptions, + opts?: AstroMarkdownOptions & { allowedRemoteDomains: string[] }, ): Promise { const { syntaxHighlight = markdownConfigDefaults.syntaxHighlight, @@ -93,7 +93,7 @@ export async function createMarkdownProcessor( if (!isPerformanceBenchmark) { // Apply later in case user plugins resolve relative image paths - parser.use(remarkCollectImages); + parser.use(remarkCollectImages, { allowedRemoteDomains: opts?.allowedRemoteDomains }); } // Remark -> Rehype diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts index 8f9b1c1ac483..c497ce285bd4 100644 --- a/packages/markdown/remark/src/remark-collect-images.ts +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -3,7 +3,13 @@ import { definitions } from 'mdast-util-definitions'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; -export function remarkCollectImages() { +interface Opts { + allowedRemoteDomains?: string[]; +} + +export function remarkCollectImages(opts?: Opts) { + const allowedRemoteDomains = new Set(opts?.allowedRemoteDomains ?? []); + return function (tree: Root, vfile: VFile) { if (typeof vfile?.path !== 'string') return; @@ -22,9 +28,14 @@ export function remarkCollectImages() { } if (!url) return; - else if (isValidUrl(url)) remoteImagePaths.add(url); - // Only optimize local images, not paths to `/public` - else if (!url.startsWith("/")) localImagePaths.add(url); + + try { + const domain = new URL(url).host; + if (allowedRemoteDomains.has(domain)) remoteImagePaths.add(url); + } catch { + // Not a valid remote URL. Check if it's a local image. If it's an absolute path, then it's not. + if (!url.startsWith('/')) localImagePaths.add(url); + } }); vfile.data.astro ??= {}; @@ -32,12 +43,3 @@ export function remarkCollectImages() { vfile.data.astro.remoteImagePaths = Array.from(remoteImagePaths); }; } - -function isValidUrl(str: string): boolean { - try { - new URL(str); - return true; - } catch { - return false; - } -} diff --git a/packages/markdown/remark/test/remark-collect-images.test.js b/packages/markdown/remark/test/remark-collect-images.test.js index 3eb3cd04bd55..8d6c3d647e20 100644 --- a/packages/markdown/remark/test/remark-collect-images.test.js +++ b/packages/markdown/remark/test/remark-collect-images.test.js @@ -6,11 +6,11 @@ describe('collect images', async () => { let processor; before(async () => { - processor = await createMarkdownProcessor(); + processor = await createMarkdownProcessor({ allowedRemoteDomains: ['example.com'] }); }); it('should collect inline image paths', async () => { - const markdown = `Hello ![inline image url](./img.png) ![inline remote image url](https://example.com/example.png)`; + const markdown = `Hello ![inline image url](./img.png)`; const fileURL = 'file.md'; const { @@ -20,13 +20,47 @@ describe('collect images', async () => { assert.equal( code, - '

Hello

', + '

Hello

', ); assert.deepEqual(localImagePaths, ['./img.png']); + assert.deepEqual(remoteImagePaths, []); + }); + + it('should collect allowed remote image paths', async () => { + const markdown = `Hello ![inline remote image url](https://example.com/example.png)`; + const fileURL = 'file.md'; + + const { + code, + metadata: { localImagePaths, remoteImagePaths }, + } = await processor.render(markdown, { fileURL }); + assert.equal( + code, + `

Hello

`, + ); + + assert.deepEqual(localImagePaths, []); assert.deepEqual(remoteImagePaths, ['https://example.com/example.png']); }); + it('should not collect other remote image paths', async () => { + const markdown = `Hello ![inline remote image url](https://google.com/google.png)`; + const fileURL = 'file.md'; + + const { + code, + metadata: { localImagePaths, remoteImagePaths }, + } = await processor.render(markdown, { fileURL }); + assert.equal( + code, + `

Hello inline remote image url

`, + ); + + assert.deepEqual(localImagePaths, []); + assert.deepEqual(remoteImagePaths, []); + }); + it('should add image paths from definition', async () => { const markdown = `Hello ![image ref][img-ref] ![remote image ref][remote-img-ref]\n\n[img-ref]: ./img.webp\n[remote-img-ref]: https://example.com/example.jpg`; const fileURL = 'file.md'; From e330109458a9248958548150965bf86677b7c971 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:27:41 -0500 Subject: [PATCH 06/17] address remark feedback This commit moves remote pattern checking functionality into @astrojs/markdown-remark, out of the main astro package, so it can be used by remark-collect-images. We do this instead of moving it into a shared @astrojs/internal-helpers because part of that package uses node APIs, and remark-collect-images needs to work in a browser. --- packages/astro/src/assets/endpoint/generic.ts | 2 +- packages/astro/src/assets/endpoint/node.ts | 2 +- packages/astro/src/assets/services/service.ts | 2 +- packages/astro/src/assets/utils/index.ts | 9 ------- packages/astro/src/content/runtime.ts | 14 +++------- packages/astro/src/types/public/config.ts | 2 +- packages/astro/src/types/public/index.ts | 2 +- .../content-entry-type.ts | 2 +- .../astro/src/vite-plugin-markdown/index.ts | 2 +- packages/markdown/remark/src/index.ts | 19 ++++++++++--- .../remark/src/remark-collect-images.ts | 27 ++++++++++--------- .../remark/src/remote-pattern.ts} | 11 +++----- packages/markdown/remark/src/types.ts | 18 +++++++++++++ .../remark/test/remark-collect-images.test.js | 2 +- .../remark/test}/remote-pattern.test.js | 4 +-- 15 files changed, 67 insertions(+), 51 deletions(-) rename packages/{astro/src/assets/utils/remotePattern.ts => markdown/remark/src/remote-pattern.ts} (88%) rename packages/{astro/test/units/assets => markdown/remark/test}/remote-pattern.test.js (96%) diff --git a/packages/astro/src/assets/endpoint/generic.ts b/packages/astro/src/assets/endpoint/generic.ts index f8924134b271..6cfa028331d1 100644 --- a/packages/astro/src/assets/endpoint/generic.ts +++ b/packages/astro/src/assets/endpoint/generic.ts @@ -1,11 +1,11 @@ // @ts-expect-error import { imageConfig } from 'astro:assets'; import { isRemotePath } from '@astrojs/internal-helpers/path'; +import { isRemoteAllowed } from '@astrojs/markdown-remark'; import * as mime from 'mrmime'; import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; -import { isRemoteAllowed } from '../utils/remotePattern.js'; async function loadRemoteImage(src: URL, headers: Headers) { try { diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts index 4b18deb38202..2b6ed37d7e30 100644 --- a/packages/astro/src/assets/endpoint/node.ts +++ b/packages/astro/src/assets/endpoint/node.ts @@ -6,11 +6,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; // @ts-expect-error import { assetsDir, imageConfig, outDir } from 'astro:assets'; import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path'; +import { isRemoteAllowed } from '@astrojs/markdown-remark'; import * as mime from 'mrmime'; import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; -import { isRemoteAllowed } from '../utils/remotePattern.js'; function replaceFileSystemReferences(src: string) { return os.platform().includes('win32') ? src.replace(/^\/@fs\//, '') : src.replace(/^\/@fs/, ''); diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index ee3bcb587f8f..3ff69999fa17 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -1,3 +1,4 @@ +import { isRemoteAllowed } from '@astrojs/markdown-remark'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import { isRemotePath, joinPaths } from '../../core/path.js'; import type { AstroConfig } from '../../types/public/config.js'; @@ -9,7 +10,6 @@ import type { UnresolvedSrcSetValue, } from '../types.js'; import { isESMImportedImage, isRemoteImage } from '../utils/imageKind.js'; -import { isRemoteAllowed } from '../utils/remotePattern.js'; export type ImageService = LocalImageService | ExternalImageService; diff --git a/packages/astro/src/assets/utils/index.ts b/packages/astro/src/assets/utils/index.ts index 3fae182000fa..d937048b5fa6 100644 --- a/packages/astro/src/assets/utils/index.ts +++ b/packages/astro/src/assets/utils/index.ts @@ -2,15 +2,6 @@ export { emitESMImage } from './node/emitAsset.js'; export { isESMImportedImage, isRemoteImage } from './imageKind.js'; export { imageMetadata } from './metadata.js'; export { getOrigQueryParams } from './queryParams.js'; -export { - isRemoteAllowed, - matchHostname, - matchPathname, - matchPattern, - matchPort, - matchProtocol, - type RemotePattern, -} from './remotePattern.js'; export { hashTransform, propsToFilename } from './transformToPath.js'; export { inferRemoteSize } from './remoteProbe.js'; export { makeSvgComponent } from './svg.js'; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 25b2113a913e..52aaec642ab4 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -416,8 +416,11 @@ async function updateImageReferencesInBody(html: string, fileName: string) { const decodedImagePath = JSON.parse(imagePath.replaceAll('"', '"')); let image: GetImageResult; - if (validURL(decodedImagePath.src)) { + if (URL.canParse(decodedImagePath.src)) { // Remote image, pass through without resolving import + // We know we should resolve this remote image because either: + // 1. It was collected with the remark-collect-images plugin, which respects the astro image configuration, + // 2. OR it was manually injected by another plugin, and we should respect that. image = await getImage(decodedImagePath); } else { const id = imageSrcToImportId(decodedImagePath.src, fileName); @@ -453,15 +456,6 @@ async function updateImageReferencesInBody(html: string, fileName: string) { }); } -function validURL(src: string): boolean { - try { - new URL(src); - return true; - } catch { - return false; - } -} - function updateImageReferencesInData>( data: T, fileName?: string, diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 84dc3fd83484..4f54fc12429f 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -3,12 +3,12 @@ import type { RehypePlugins, RemarkPlugins, RemarkRehype, + RemotePattern, ShikiConfig, } from '@astrojs/markdown-remark'; import type { BuiltinDriverName, BuiltinDriverOptions, Driver, Storage } from 'unstorage'; import type { UserConfig as OriginalViteUserConfig, SSROptions as ViteSSROptions } from 'vite'; import type { ImageFit, ImageLayout } from '../../assets/types.js'; -import type { RemotePattern } from '../../assets/utils/remotePattern.js'; import type { SvgRenderMode } from '../../assets/utils/svg.js'; import type { AssetsPrefix } from '../../core/app/types.js'; import type { AstroConfigType } from '../../core/config/schema.js'; diff --git a/packages/astro/src/types/public/index.ts b/packages/astro/src/types/public/index.ts index fae134bbeb01..af806805cb6e 100644 --- a/packages/astro/src/types/public/index.ts +++ b/packages/astro/src/types/public/index.ts @@ -18,6 +18,7 @@ export type { MarkdownHeading, RehypePlugins, RemarkPlugins, + RemotePattern, ShikiConfig, } from '@astrojs/markdown-remark'; export type { @@ -35,7 +36,6 @@ export type { ImageTransform, UnresolvedImageTransform, } from '../../assets/types.js'; -export type { RemotePattern } from '../../assets/utils/remotePattern.js'; export type { AssetsPrefix, SSRManifest } from '../../core/app/types.js'; export type { AstroCookieGetOptions, diff --git a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts index c7018b623668..a8832ef35630 100644 --- a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts +++ b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts @@ -19,7 +19,7 @@ export const markdownContentEntryType: ContentEntryType = { async getRenderFunction(config) { const processor = await createMarkdownProcessor({ - allowedRemoteDomains: config.image.domains, + image: config.image, ...config.markdown, }); return async function renderToString(entry) { diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 5d5c622f14a6..9cd080a0302d 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -61,7 +61,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug // Lazily initialize the Markdown processor if (!processor) { processor = createMarkdownProcessor({ - allowedRemoteDomains: settings.config.image.domains, + image: settings.config.image, ...settings.config.markdown, }); } diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 3ea2f88d14d2..c3b15661b6c0 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -1,4 +1,8 @@ -import type { AstroMarkdownOptions, MarkdownProcessor } from './types.js'; +import type { + AstroMarkdownOptions, + AstroMarkdownProcessorOptions, + MarkdownProcessor, +} from './types.js'; import { loadPlugins } from './load-plugins.js'; import { rehypeHeadingIds } from './rehype-collect-headings.js'; @@ -27,6 +31,15 @@ export { type ParseFrontmatterOptions, type ParseFrontmatterResult, } from './frontmatter.js'; +export { + matchPattern, + matchPort, + matchProtocol, + matchHostname, + matchPathname, + isRemoteAllowed, + type RemotePattern, +} from './remote-pattern.js'; export { createShikiHighlighter, type ShikiHighlighter, @@ -59,7 +72,7 @@ const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK); * Create a markdown preprocessor to render multiple markdown files */ export async function createMarkdownProcessor( - opts?: AstroMarkdownOptions & { allowedRemoteDomains: string[] }, + opts?: AstroMarkdownProcessorOptions, ): Promise { const { syntaxHighlight = markdownConfigDefaults.syntaxHighlight, @@ -93,7 +106,7 @@ export async function createMarkdownProcessor( if (!isPerformanceBenchmark) { // Apply later in case user plugins resolve relative image paths - parser.use(remarkCollectImages, { allowedRemoteDomains: opts?.allowedRemoteDomains }); + parser.use(remarkCollectImages, opts?.image); } // Remark -> Rehype diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts index c497ce285bd4..632a70286581 100644 --- a/packages/markdown/remark/src/remark-collect-images.ts +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -2,13 +2,12 @@ import type { Root } from 'mdast'; import { definitions } from 'mdast-util-definitions'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; +import { isRemoteAllowed } from './remote-pattern.js'; +import type { AstroMarkdownProcessorOptions } from './types.js'; -interface Opts { - allowedRemoteDomains?: string[]; -} - -export function remarkCollectImages(opts?: Opts) { - const allowedRemoteDomains = new Set(opts?.allowedRemoteDomains ?? []); +export function remarkCollectImages(opts: AstroMarkdownProcessorOptions['image']) { + const domains = opts?.domains ?? []; + const remotePatterns = opts?.remotePatterns ?? []; return function (tree: Root, vfile: VFile) { if (typeof vfile?.path !== 'string') return; @@ -29,12 +28,16 @@ export function remarkCollectImages(opts?: Opts) { if (!url) return; - try { - const domain = new URL(url).host; - if (allowedRemoteDomains.has(domain)) remoteImagePaths.add(url); - } catch { - // Not a valid remote URL. Check if it's a local image. If it's an absolute path, then it's not. - if (!url.startsWith('/')) localImagePaths.add(url); + if (URL.canParse(url)) { + if (isRemoteAllowed(url, { domains, remotePatterns })) { + remoteImagePaths.add(url); + } + } else if (!url.startsWith('/')) { + // If: + // + not a valid URL + // + AND not an absolute path + // Then it's a local image. + localImagePaths.add(url); } }); diff --git a/packages/astro/src/assets/utils/remotePattern.ts b/packages/markdown/remark/src/remote-pattern.ts similarity index 88% rename from packages/astro/src/assets/utils/remotePattern.ts rename to packages/markdown/remark/src/remote-pattern.ts index d3e832573a7f..768ed59a389b 100644 --- a/packages/astro/src/assets/utils/remotePattern.ts +++ b/packages/markdown/remark/src/remote-pattern.ts @@ -1,5 +1,4 @@ -import { isRemotePath } from '@astrojs/internal-helpers/path'; -import type { AstroConfig } from '../../types/public/config.js'; +import type { AstroMarkdownProcessorOptions } from './types.js'; export type RemotePattern = { hostname?: string; @@ -68,12 +67,10 @@ export function matchPathname(url: URL, pathname?: string, allowWildcard?: boole export function isRemoteAllowed( src: string, { - domains = [], - remotePatterns = [], - }: Partial>, + domains, + remotePatterns, + }: Required['image'], 'domains' | 'remotePatterns'>>, ): boolean { - if (!isRemotePath(src)) return false; - const url = new URL(src); return ( domains.some((domain) => matchHostname(url, domain)) || diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index af6709ecd698..0d700f2be8bc 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -40,6 +40,9 @@ export interface ShikiConfig extends Pick, Pick {} +/** + * Configuration options that end up in the markdown section of AstroConfig + */ export interface AstroMarkdownOptions { syntaxHighlight?: 'shiki' | 'prism' | false; shikiConfig?: ShikiConfig; @@ -50,6 +53,21 @@ export interface AstroMarkdownOptions { smartypants?: boolean; } +/** + * Extra configuration options from other parts of AstroConfig that get injected into this plugin + */ +export interface AstroMarkdownProcessorOptions extends AstroMarkdownOptions { + image?: { + domains?: string[]; + remotePatterns?: Array<{ + protocol?: string; + hostname?: string; + port?: string; + pathname?: string; + }>; + }; +} + export interface MarkdownProcessor { render: ( content: string, diff --git a/packages/markdown/remark/test/remark-collect-images.test.js b/packages/markdown/remark/test/remark-collect-images.test.js index 8d6c3d647e20..e7ac08918d86 100644 --- a/packages/markdown/remark/test/remark-collect-images.test.js +++ b/packages/markdown/remark/test/remark-collect-images.test.js @@ -6,7 +6,7 @@ describe('collect images', async () => { let processor; before(async () => { - processor = await createMarkdownProcessor({ allowedRemoteDomains: ['example.com'] }); + processor = await createMarkdownProcessor({ image: { domains: ['example.com'] } }); }); it('should collect inline image paths', async () => { diff --git a/packages/astro/test/units/assets/remote-pattern.test.js b/packages/markdown/remark/test/remote-pattern.test.js similarity index 96% rename from packages/astro/test/units/assets/remote-pattern.test.js rename to packages/markdown/remark/test/remote-pattern.test.js index 0961ffd05232..693d6b3aba97 100644 --- a/packages/astro/test/units/assets/remote-pattern.test.js +++ b/packages/markdown/remark/test/remote-pattern.test.js @@ -6,9 +6,9 @@ import { matchPattern, matchPort, matchProtocol, -} from '../../../dist/assets/utils/remotePattern.js'; +} from '../dist/remote-pattern.js'; -describe('astro/src/assets/utils/remotePattern', () => { +describe('remote-pattern', () => { const url1 = new URL('https://docs.astro.build/en/getting-started'); const url2 = new URL('http://preview.docs.astro.build:8080/'); const url3 = new URL('https://astro.build/'); From a93ec1bfc753ea57fa6cdfa7920b9dbdd567f378 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:40:07 -0500 Subject: [PATCH 07/17] rehype feedback --- packages/markdown/remark/src/index.ts | 2 +- packages/markdown/remark/src/rehype-images.ts | 81 +++++++++---------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index c3b15661b6c0..57fff07aec90 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -131,7 +131,7 @@ export async function createMarkdownProcessor( } // Images / Assets support - parser.use(rehypeImages()); + parser.use(rehypeImages); // Headings if (!isPerformanceBenchmark) { diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index 2a1476441450..7f1bf30bcdba 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -1,47 +1,44 @@ +import type { Properties } from 'hast'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; export function rehypeImages() { - return () => - function (tree: any, file: VFile) { - const imageOccurrenceMap = new Map(); - - visit(tree, (node) => { - if (node.type !== 'element') return; - if (node.tagName !== 'img') return; - - if (node.properties?.src) { - node.properties.src = decodeURI(node.properties.src); - - if (file.data.astro?.localImagePaths?.includes(node.properties.src)) { - const { ...props } = node.properties; - - // Initialize or increment occurrence count for this image - const index = imageOccurrenceMap.get(node.properties.src) || 0; - imageOccurrenceMap.set(node.properties.src, index + 1); - - node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...props, index }); - - Object.keys(props).forEach((prop) => { - delete node.properties[prop]; - }); - } else if (file.data.astro?.remoteImagePaths?.includes(node.properties.src)) { - const { ...props } = node.properties; - - const index = imageOccurrenceMap.get(node.properties.src) || 0; - imageOccurrenceMap.set(node.properties.src, index + 1); - - node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ - inferSize: 'width' in props && 'height' in props ? undefined : true, - ...props, - index, - }); - - Object.keys(props).forEach((prop) => { - delete node.properties[prop]; - }); - } - } - }); - }; + return function (tree: any, file: VFile) { + if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length) { + // No images to transform, nothing to do. + return; + } + + const imageOccurrenceMap = new Map(); + + visit(tree, 'element', (node) => { + if (node.tagName !== 'img') return; + if (!node.properties?.src) return; + + const src = decodeURI(node.properties.src); + let newProperties: Properties; + + if (file.data.astro?.localImagePaths?.includes(src)) { + // Override the original `src` with the new, decoded `src` that Astro will better understand. + newProperties = { ...node.properties, src }; + } else if (file.data.astro?.remoteImagePaths?.includes(src)) { + newProperties = { + // By default, markdown images won't have width and height set. However, just in case another user plugin does set these, we should respect them. + inferSize: 'width' in node.properties && 'height' in node.properties ? undefined : true, + ...node.properties, + src, + }; + } else { + // Not in localImagePaths or remoteImagePaths, we should not transform. + return; + } + + // Initialize or increment occurrence count for this image + const index = imageOccurrenceMap.get(node.properties.src) || 0; + imageOccurrenceMap.set(node.properties.src, index + 1); + + // Set a special property on the image so later Astro code knows to process this image. + node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...newProperties, index }); + }); + }; } From 65a9cc51183b0ebb33ef7d65a96ba4de3c6331a1 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:46:47 -0500 Subject: [PATCH 08/17] fix test --- packages/markdown/remark/src/rehype-images.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index 7f1bf30bcdba..7a2f98c41e6c 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -38,7 +38,7 @@ export function rehypeImages() { imageOccurrenceMap.set(node.properties.src, index + 1); // Set a special property on the image so later Astro code knows to process this image. - node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...newProperties, index }); + node.properties = { __ASTRO_IMAGE_: JSON.stringify({ ...newProperties, index }) }; }); }; } From 9a4ab798e505b2b0258878e8a7766fe0c74d1241 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:50:21 -0500 Subject: [PATCH 09/17] better rehype-images typechecking --- packages/markdown/remark/src/rehype-images.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index 7a2f98c41e6c..92043b5e3ad8 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -1,9 +1,9 @@ -import type { Properties } from 'hast'; +import type { Properties, Root } from 'hast'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; export function rehypeImages() { - return function (tree: any, file: VFile) { + return function (tree: Root, file: VFile) { if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length) { // No images to transform, nothing to do. return; @@ -13,7 +13,7 @@ export function rehypeImages() { visit(tree, 'element', (node) => { if (node.tagName !== 'img') return; - if (!node.properties?.src) return; + if (typeof node.properties?.src !== 'string') return; const src = decodeURI(node.properties.src); let newProperties: Properties; From 8b408579e7caf41746795b08da7e0a82da29e24a Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:57:11 -0500 Subject: [PATCH 10/17] fix test 2 This time, the problem is certain parts of `astro` don't like depending on `@astrojs/markdown-remark`, because it brings in `node:fs` somehow. This on its own is probably a problem, but I think I'll work around it by moving the relevant shared code into `@astrojs/internal-helpers/remote` so that we can stay node-free. --- packages/astro/src/assets/endpoint/generic.ts | 2 +- packages/astro/src/assets/endpoint/node.ts | 2 +- packages/astro/src/assets/services/service.ts | 2 +- packages/astro/src/types/public/config.ts | 4 +- packages/astro/src/types/public/index.ts | 4 +- .../test/units}/remote-pattern.test.js | 2 +- packages/internal-helpers/package.json | 4 ++ .../src/remote.ts} | 51 ++++++++++++------- packages/markdown/remark/package.json | 1 + packages/markdown/remark/src/index.ts | 9 ---- .../remark/src/remark-collect-images.ts | 2 +- packages/markdown/remark/src/types.ts | 8 +-- pnpm-lock.yaml | 3 ++ 13 files changed, 55 insertions(+), 39 deletions(-) rename packages/{markdown/remark/test => astro/test/units}/remote-pattern.test.js (98%) rename packages/{markdown/remark/src/remote-pattern.ts => internal-helpers/src/remote.ts} (63%) diff --git a/packages/astro/src/assets/endpoint/generic.ts b/packages/astro/src/assets/endpoint/generic.ts index 6cfa028331d1..d71d06987073 100644 --- a/packages/astro/src/assets/endpoint/generic.ts +++ b/packages/astro/src/assets/endpoint/generic.ts @@ -1,7 +1,7 @@ // @ts-expect-error import { imageConfig } from 'astro:assets'; import { isRemotePath } from '@astrojs/internal-helpers/path'; -import { isRemoteAllowed } from '@astrojs/markdown-remark'; +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; import * as mime from 'mrmime'; import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts index 2b6ed37d7e30..991d7171f3ec 100644 --- a/packages/astro/src/assets/endpoint/node.ts +++ b/packages/astro/src/assets/endpoint/node.ts @@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; // @ts-expect-error import { assetsDir, imageConfig, outDir } from 'astro:assets'; import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path'; -import { isRemoteAllowed } from '@astrojs/markdown-remark'; +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; import * as mime from 'mrmime'; import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index 3ff69999fa17..4bb643a9cdfe 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -1,4 +1,4 @@ -import { isRemoteAllowed } from '@astrojs/markdown-remark'; +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import { isRemotePath, joinPaths } from '../../core/path.js'; import type { AstroConfig } from '../../types/public/config.js'; diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 4f54fc12429f..3bb0e58819e9 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -1,9 +1,11 @@ import type { OutgoingHttpHeaders } from 'node:http'; +import type { + RemotePattern +} from '@astrojs/internal-helpers/remote'; import type { RehypePlugins, RemarkPlugins, RemarkRehype, - RemotePattern, ShikiConfig, } from '@astrojs/markdown-remark'; import type { BuiltinDriverName, BuiltinDriverOptions, Driver, Storage } from 'unstorage'; diff --git a/packages/astro/src/types/public/index.ts b/packages/astro/src/types/public/index.ts index af806805cb6e..53b67aa3f94b 100644 --- a/packages/astro/src/types/public/index.ts +++ b/packages/astro/src/types/public/index.ts @@ -14,11 +14,13 @@ export type * from './manifest.js'; export type { AstroIntegrationLogger } from '../../core/logger/core.js'; export type { ToolbarServerHelpers } from '../../runtime/client/dev-toolbar/helpers.js'; +export type { + RemotePattern, +} from '@astrojs/internal-helpers/remote'; export type { MarkdownHeading, RehypePlugins, RemarkPlugins, - RemotePattern, ShikiConfig, } from '@astrojs/markdown-remark'; export type { diff --git a/packages/markdown/remark/test/remote-pattern.test.js b/packages/astro/test/units/remote-pattern.test.js similarity index 98% rename from packages/markdown/remark/test/remote-pattern.test.js rename to packages/astro/test/units/remote-pattern.test.js index 693d6b3aba97..95879d89b6e2 100644 --- a/packages/markdown/remark/test/remote-pattern.test.js +++ b/packages/astro/test/units/remote-pattern.test.js @@ -6,7 +6,7 @@ import { matchPattern, matchPort, matchProtocol, -} from '../dist/remote-pattern.js'; +} from '@astrojs/internal-helpers/remote'; describe('remote-pattern', () => { const url1 = new URL('https://docs.astro.build/en/getting-started'); diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json index 5a3960e06c49..aa58997cd2a5 100644 --- a/packages/internal-helpers/package.json +++ b/packages/internal-helpers/package.json @@ -13,6 +13,7 @@ "bugs": "https://github.com/withastro/astro/issues", "exports": { "./path": "./dist/path.js", + "./remote": "./dist/remote.js", "./fs": "./dist/fs.js" }, "typesVersions": { @@ -20,6 +21,9 @@ "path": [ "./dist/path.d.ts" ], + "remote": [ + "./dist/remote.d.ts" + ], "fs": [ "./dist/fs.d.ts" ] diff --git a/packages/markdown/remark/src/remote-pattern.ts b/packages/internal-helpers/src/remote.ts similarity index 63% rename from packages/markdown/remark/src/remote-pattern.ts rename to packages/internal-helpers/src/remote.ts index 768ed59a389b..0023deff843e 100644 --- a/packages/markdown/remark/src/remote-pattern.ts +++ b/packages/internal-helpers/src/remote.ts @@ -1,5 +1,3 @@ -import type { AstroMarkdownProcessorOptions } from './types.js'; - export type RemotePattern = { hostname?: string; pathname?: string; @@ -24,19 +22,25 @@ export function matchProtocol(url: URL, protocol?: string) { return !protocol || protocol === url.protocol.slice(0, -1); } -export function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean) { +export function matchHostname( + url: URL, + hostname?: string, + allowWildcard?: boolean, +) { if (!hostname) { return true; - } else if (!allowWildcard || !hostname.startsWith('*')) { + } else if (!allowWildcard || !hostname.startsWith("*")) { return hostname === url.hostname; - } else if (hostname.startsWith('**.')) { + } else if (hostname.startsWith("**.")) { const slicedHostname = hostname.slice(2); // ** length - return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname); - } else if (hostname.startsWith('*.')) { + return ( + slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname) + ); + } else if (hostname.startsWith("*.")) { const slicedHostname = hostname.slice(1); // * length const additionalSubdomains = url.hostname - .replace(slicedHostname, '') - .split('.') + .replace(slicedHostname, "") + .split(".") .filter(Boolean); return additionalSubdomains.length === 1; } @@ -44,19 +48,25 @@ export function matchHostname(url: URL, hostname?: string, allowWildcard?: boole return false; } -export function matchPathname(url: URL, pathname?: string, allowWildcard?: boolean) { +export function matchPathname( + url: URL, + pathname?: string, + allowWildcard?: boolean, +) { if (!pathname) { return true; - } else if (!allowWildcard || !pathname.endsWith('*')) { + } else if (!allowWildcard || !pathname.endsWith("*")) { return pathname === url.pathname; - } else if (pathname.endsWith('/**')) { + } else if (pathname.endsWith("/**")) { const slicedPathname = pathname.slice(0, -2); // ** length - return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname); - } else if (pathname.endsWith('/*')) { + return ( + slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname) + ); + } else if (pathname.endsWith("/*")) { const slicedPathname = pathname.slice(0, -1); // * length const additionalPathChunks = url.pathname - .replace(slicedPathname, '') - .split('/') + .replace(slicedPathname, "") + .split("/") .filter(Boolean); return additionalPathChunks.length === 1; } @@ -69,8 +79,15 @@ export function isRemoteAllowed( { domains, remotePatterns, - }: Required['image'], 'domains' | 'remotePatterns'>>, + }: { + domains: string[]; + remotePatterns: RemotePattern[]; + }, ): boolean { + if (!URL.canParse(src)) { + return false; + } + const url = new URL(src); return ( domains.some((domain) => matchHostname(url, domain)) || diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index 8e9e142a625e..5ad883a735ae 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -32,6 +32,7 @@ "test": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { + "@astrojs/internal-helpers": "workspace:*", "@astrojs/prism": "workspace:*", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 57fff07aec90..d1b6035e4adf 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -31,15 +31,6 @@ export { type ParseFrontmatterOptions, type ParseFrontmatterResult, } from './frontmatter.js'; -export { - matchPattern, - matchPort, - matchProtocol, - matchHostname, - matchPathname, - isRemoteAllowed, - type RemotePattern, -} from './remote-pattern.js'; export { createShikiHighlighter, type ShikiHighlighter, diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts index 632a70286581..062fabc455c6 100644 --- a/packages/markdown/remark/src/remark-collect-images.ts +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -2,7 +2,7 @@ import type { Root } from 'mdast'; import { definitions } from 'mdast-util-definitions'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; -import { isRemoteAllowed } from './remote-pattern.js'; +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; import type { AstroMarkdownProcessorOptions } from './types.js'; export function remarkCollectImages(opts: AstroMarkdownProcessorOptions['image']) { diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index 0d700f2be8bc..91dd00607527 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -3,6 +3,7 @@ import type * as mdast from 'mdast'; import type { Options as RemarkRehypeOptions } from 'remark-rehype'; import type { BuiltinTheme } from 'shiki'; import type * as unified from 'unified'; +import type { RemotePattern } from '@astrojs/internal-helpers/remote'; import type { CreateShikiHighlighterOptions, ShikiHighlighterHighlightOptions } from './shiki.js'; export type { Node } from 'unist'; @@ -59,12 +60,7 @@ export interface AstroMarkdownOptions { export interface AstroMarkdownProcessorOptions extends AstroMarkdownOptions { image?: { domains?: string[]; - remotePatterns?: Array<{ - protocol?: string; - hostname?: string; - port?: string; - pathname?: string; - }>; + remotePatterns?: RemotePattern[]; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b52eebfc8dc9..3e4529f4d42d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6064,6 +6064,9 @@ importers: packages/markdown/remark: dependencies: + '@astrojs/internal-helpers': + specifier: workspace:* + version: link:../../internal-helpers '@astrojs/prism': specifier: workspace:* version: link:../../astro-prism From deb478abd208eedde9e1978e022b59652b87ecbb Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Wed, 19 Feb 2025 08:22:21 -0500 Subject: [PATCH 11/17] update changeset --- .changeset/shy-bats-exist.md | 17 +++++++++++++++++ .changeset/thick-jeans-trade.md | 9 --------- 2 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 .changeset/shy-bats-exist.md delete mode 100644 .changeset/thick-jeans-trade.md diff --git a/.changeset/shy-bats-exist.md b/.changeset/shy-bats-exist.md new file mode 100644 index 000000000000..0be63bb4c993 --- /dev/null +++ b/.changeset/shy-bats-exist.md @@ -0,0 +1,17 @@ +--- +'@astrojs/mdx': minor +'@astrojs/internal-helpers': minor +'@astrojs/markdown-remark': minor +'astro': minor +--- + +Pass remote Markdown images through image service + +Previously, Astro only allowed local images to be optimized when included using +`![]()` syntax in plain Markdown files. This was because, when the image +service was first introduced, it only was able to optimize those images. Now, +however, Astro's image service can optimize remote images as well. So, we can +add support for this! + +This is a semver-minor bump because it can significantly change what's output +under certain circumstances. diff --git a/.changeset/thick-jeans-trade.md b/.changeset/thick-jeans-trade.md deleted file mode 100644 index b209a5d7c63f..000000000000 --- a/.changeset/thick-jeans-trade.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@astrojs/mdx': minor -'@astrojs/markdown-remark': minor -'astro': minor ---- - -Optimize remote images in Markdown files - -Previously, Astro only allowed local images to be optimized when included using `![]()` syntax in plain Markdown files. This was because, when the image service was first introduced, it only was able to optimize those images. Now, however, Astro's image service can optimize remote images as well. So, we can add support for this now! \ No newline at end of file From 2f4e959f5aefe716a571cf8bdfa11a78ad5b6e1c Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:57:50 -0500 Subject: [PATCH 12/17] Update .changeset/shy-bats-exist.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/shy-bats-exist.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.changeset/shy-bats-exist.md b/.changeset/shy-bats-exist.md index 0be63bb4c993..ecb1cc90e36a 100644 --- a/.changeset/shy-bats-exist.md +++ b/.changeset/shy-bats-exist.md @@ -5,13 +5,10 @@ 'astro': minor --- -Pass remote Markdown images through image service +Adds the ability to process and optimize remote images in Markdown files -Previously, Astro only allowed local images to be optimized when included using -`![]()` syntax in plain Markdown files. This was because, when the image -service was first introduced, it only was able to optimize those images. Now, -however, Astro's image service can optimize remote images as well. So, we can -add support for this! +Previously, Astro only allowed local images to be optimized when included using `![]()` syntax in plain Markdown files. Astro's image service could only display remote images without any processing. -This is a semver-minor bump because it can significantly change what's output -under certain circumstances. +Now, Astro's image service can also optimize remote images written in standard Markdown syntax. This allows you to enjoy the benefits of Astro's image processing when your images are stored externally, for example in a CMS or digital asset manager. + +No additional configuration is required to use this feature! Any existing remote images written in Markdown will now automatically be optimized. To opt-out of this processing, write your images in Markdown using the HTML `` tag instead. Note that images located in your `public/` folder are still never processed. From 63dab1890afabeb7d4510a388d304e10b91e3c26 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:10:15 -0500 Subject: [PATCH 13/17] separate changesets --- .changeset/quiet-birds-joke.md | 9 +++++++++ .changeset/shy-bats-exist.md | 3 --- .changeset/tiny-cows-march.md | 11 +++++++++++ .changeset/warm-planes-swim.md | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 .changeset/quiet-birds-joke.md create mode 100644 .changeset/tiny-cows-march.md create mode 100644 .changeset/warm-planes-swim.md diff --git a/.changeset/quiet-birds-joke.md b/.changeset/quiet-birds-joke.md new file mode 100644 index 000000000000..5ca77a64cd7e --- /dev/null +++ b/.changeset/quiet-birds-joke.md @@ -0,0 +1,9 @@ +--- +'@astrojs/internal-helpers': minor +--- + +Add remote URL filtering utilities + +Code to filter remote URLs according to a given config is now used by both +`astro` and `@astrojs/markdown-remark`. That logic should be shared between +those packages, so it must live here. diff --git a/.changeset/shy-bats-exist.md b/.changeset/shy-bats-exist.md index ecb1cc90e36a..25ac954ef2af 100644 --- a/.changeset/shy-bats-exist.md +++ b/.changeset/shy-bats-exist.md @@ -1,7 +1,4 @@ --- -'@astrojs/mdx': minor -'@astrojs/internal-helpers': minor -'@astrojs/markdown-remark': minor 'astro': minor --- diff --git a/.changeset/tiny-cows-march.md b/.changeset/tiny-cows-march.md new file mode 100644 index 000000000000..9e4055a55afa --- /dev/null +++ b/.changeset/tiny-cows-march.md @@ -0,0 +1,11 @@ +--- +'@astrojs/mdx': minor +--- + +Adds the ability to process and optimize remote images in Markdown syntax in MDX files. + +Previously, Astro only allowed local images to be optimized when included using `![]()` syntax. Astro's image service could only display remote images without any processing. + +Now, Astro's image service can also optimize remote images written in standard Markdown syntax. This allows you to enjoy the benefits of Astro's image processing when your images are stored externally, for example in a CMS or digital asset manager. + +No additional configuration is required to use this feature! Any existing remote images written in Markdown will now automatically be optimized. To opt-out of this processing, write your images in Markdown using the HTML `` tag instead. Note that images located in your `public/` folder are still never processed. diff --git a/.changeset/warm-planes-swim.md b/.changeset/warm-planes-swim.md new file mode 100644 index 000000000000..b543c391d828 --- /dev/null +++ b/.changeset/warm-planes-swim.md @@ -0,0 +1,14 @@ +--- +'@astrojs/markdown-remark': minor +--- + +Transform remote images in addition to local images + +Previously, an internal remark plugin only looked for images in `![]()` syntax +that referred to a relative path, passing through to an internal rehype plugin +that would transform them for later processing by Astro's image service. + +Now, the plugins transform both local and remote images, outputting them into +`localImagePaths` and `remoteImagePaths` metadata fields. A new configuration +option, mirroring the one used by Astro, can be provided to control which +remote images get processed this way. From 2419bfb87543e8d3ec6b44ade26e6e9dbed4dd98 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:49:39 -0500 Subject: [PATCH 14/17] JSX tag Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/tiny-cows-march.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tiny-cows-march.md b/.changeset/tiny-cows-march.md index 9e4055a55afa..95682c85b0a7 100644 --- a/.changeset/tiny-cows-march.md +++ b/.changeset/tiny-cows-march.md @@ -8,4 +8,4 @@ Previously, Astro only allowed local images to be optimized when included using Now, Astro's image service can also optimize remote images written in standard Markdown syntax. This allows you to enjoy the benefits of Astro's image processing when your images are stored externally, for example in a CMS or digital asset manager. -No additional configuration is required to use this feature! Any existing remote images written in Markdown will now automatically be optimized. To opt-out of this processing, write your images in Markdown using the HTML `` tag instead. Note that images located in your `public/` folder are still never processed. +No additional configuration is required to use this feature! Any existing remote images written in Markdown will now automatically be optimized. To opt-out of this processing, write your images in Markdown using the JSX `` tag instead. Note that images located in your `public/` folder are still never processed. From acb90762aa319c1c0afd2d2870a115fc591bdd26 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:55:49 -0500 Subject: [PATCH 15/17] Update quiet-birds-joke.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/quiet-birds-joke.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.changeset/quiet-birds-joke.md b/.changeset/quiet-birds-joke.md index 5ca77a64cd7e..37b175850916 100644 --- a/.changeset/quiet-birds-joke.md +++ b/.changeset/quiet-birds-joke.md @@ -2,8 +2,6 @@ '@astrojs/internal-helpers': minor --- -Add remote URL filtering utilities +Adds remote URL filtering utilities -Code to filter remote URLs according to a given config is now used by both -`astro` and `@astrojs/markdown-remark`. That logic should be shared between -those packages, so it must live here. +This adds logic to filter remote URLs so that it can be used by both `astro` and `@astrojs/markdown-remark`. From 926ac77140e1348423a59b1cdc2265c1b3514408 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:46:59 -0500 Subject: [PATCH 16/17] Update warm-planes-swim.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/warm-planes-swim.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.changeset/warm-planes-swim.md b/.changeset/warm-planes-swim.md index b543c391d828..f3cd202e87d7 100644 --- a/.changeset/warm-planes-swim.md +++ b/.changeset/warm-planes-swim.md @@ -2,13 +2,10 @@ '@astrojs/markdown-remark': minor --- -Transform remote images in addition to local images +Adds remote image optimization in Markdown -Previously, an internal remark plugin only looked for images in `![]()` syntax -that referred to a relative path, passing through to an internal rehype plugin -that would transform them for later processing by Astro's image service. +Previously, an internal remark plugin only looked for images in `![]()` syntax that referred to a relative file path. This meant that only local images stored in `src/` were passed through to an internal rehype plugin that would transform them for later processing by Astro's image service. -Now, the plugins transform both local and remote images, outputting them into -`localImagePaths` and `remoteImagePaths` metadata fields. A new configuration -option, mirroring the one used by Astro, can be provided to control which -remote images get processed this way. +Now, the plugins recognize and transform both local and remote images using this syntax. + +While not configurable at this time, this process outputs two separate metadata fields (`localImagePaths` and `remoteImagePaths`) which allow for the possibility of controlling the behavior of each type of image separately in the future. From a21e0b5a80c00a944c241e46c6a2d20b0d6fdf76 Mon Sep 17 00:00:00 2001 From: PolyWolf <31190026+p0lyw0lf@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:51:30 -0500 Subject: [PATCH 17/17] Update warm-planes-swim.md --- .changeset/warm-planes-swim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/warm-planes-swim.md b/.changeset/warm-planes-swim.md index f3cd202e87d7..f710f1a6e3a3 100644 --- a/.changeset/warm-planes-swim.md +++ b/.changeset/warm-planes-swim.md @@ -6,6 +6,6 @@ Adds remote image optimization in Markdown Previously, an internal remark plugin only looked for images in `![]()` syntax that referred to a relative file path. This meant that only local images stored in `src/` were passed through to an internal rehype plugin that would transform them for later processing by Astro's image service. -Now, the plugins recognize and transform both local and remote images using this syntax. +Now, the plugins recognize and transform both local and remote images using this syntax. Only [authorized remote images specified in your config](https://docs.astro.build/en/guides/images/#authorizing-remote-images) are transformed; remote images from other sources will not be processed. While not configurable at this time, this process outputs two separate metadata fields (`localImagePaths` and `remoteImagePaths`) which allow for the possibility of controlling the behavior of each type of image separately in the future.