How to Download External Images Locally for Optimization with Next.js

next-export-optimize-images automatically downloads remote images, stores them in your build output, and processes them through Sharp-based optimization during static export.

The dc7290/next-export-optimize-images library extends Next.js's standard image workflow to handle external URLs. Instead of manually downloading remote assets before building, you declare the URLs in your configuration, and the CLI fetches, caches, and optimizes them alongside your local images.

The Three-Step External Image Pipeline

The library handles remote images through a distinct pipeline implemented across several core modules. Understanding this flow helps debug issues and optimize build performance.

1. Manifest Generation with External URLs

In src/cli/index.ts, the CLI constructs a manifest that bridges the gap between remote sources and local optimization. When you provide the remoteImages option, the code calls buildOutputInfo (from src/utils/buildOutputInfo.ts) for each URL to generate size variants. Each entry in the resulting manifest includes an externalUrl field containing the original remote address, plus the target output path where the file should live locally (lines 84‑104, 185‑210).

2. Network Fetch via the Downloader

Before any image processing begins, the externalImagesDownloader function (located in src/cli/external-images/index.ts) reads the manifest. It filters for entries containing externalUrl, fetches each image over HTTP, and writes the bytes to disk under the directory specified by externalImageDir (default '_next/static/media'). This module respects the throttling parameters remoteImagesDownloadsDelay and processingConcurrency to prevent overwhelming external servers or hitting rate limits.

3. Standard Optimization Pass

Once downloaded, the images exist as local files. The CLI then proceeds to getOptimizeResult, passing the same manifest entries—now with local src paths—to the Sharp-based optimization pipeline. From this point onward, remote and local images follow identical processing logic, including format conversion, quality compression, and responsive variant generation.

Configuring Remote Images in export-images.config.js

The configuration loader in src/utils/getConfig.ts supports both static arrays and dynamic async functions for the remoteImages field. This flexibility allows you to fetch image lists from CMS APIs at build time.

Basic Static Configuration

/** @type {import('next-export-optimize-images').Config} */
const config = {
  remoteImages: [
    'https://picsum.photos/id/237/800/600.jpg',
    'https://picsum.photos/id/238/800/600.jpg',
  ],
  quality: 80,
}
module.exports = config

Dynamic Async Configuration

const config = {
  remoteImages: async () => {
    const response = await fetch('https://api.example.com/images');
    const data = await response.json();
    return data.map(img => img.url);
  },
}
module.exports = config

The library evaluates stringified functions for advanced use cases like filenameGenerator and sourceImageParser, but remoteImages can be provided as a standard array or async function returning an array of URL strings.

CLI Execution and Manifest Structure

When you run npx next-export-optimize-images, the entry point at src/cli/index.ts orchestrates the workflow. It first generates an ND‑JSON manifest file where each line represents an image variant. Entries originating from external sources carry the externalUrl property.

Example Manifest Entry

{
  "output": "/_next/static/media/7c4a8c1e2d3f4a5b6c7d8e9f0a1b2c3d.jpg",
  "src": "/_next/static/media/7c4a8c1e2d3f4a5b6c7d8e9f0a1b2c3d.jpg",
  "width": 800,
  "extension": "jpeg",
  "externalUrl": "https://picsum.photos/id/237/800/600.jpg"
}

The src field points to the local cache location (prefixed according to your build mode), while externalUrl signals to the downloader that a network fetch is required. After externalImagesDownloader completes, the manifest is processed by processManifest (src/utils/processManifest.ts) and fed into the optimization worker pool.

Throttling and Concurrency Controls

Downloading numerous large images simultaneously can trigger rate limiting or memory pressure. The library provides two configuration options to manage this, both passed directly to the downloader module:

  • remoteImagesDownloadsDelay: Milliseconds to wait between individual downloads (default: 0)
  • processingConcurrency: Maximum number of concurrent downloads (default: system-dependent)
const config = {
  remoteImages: ['https://example.com/gallery/1.jpg', 'https://example.com/gallery/2.jpg'],
  remoteImagesDownloadsDelay: 200,  // Wait 200ms between each request
  processingConcurrency: 5,         // Download max 5 images simultaneously
}

These settings are particularly important when sourcing hundreds of images from a single domain or when running in resource-constrained CI environments.

Customizing the Download Directory

By default, external images are stored in _next/static/media within your output directory. You can redirect this to the public folder if you need the raw files included in your static export for non-Next.js consumption:

const config = {
  externalImageDir: 'public/remote',
  remoteImages: ['https://example.com/logo.png'],
}

This writes files to public/remote/<hash>.png, making them available at /remote/<hash>.png after export while still allowing the optimizer to generate compressed variants in the standard _next directory.

Summary

  • Declare external URLs in export-images.config.js using the remoteImages option, which accepts arrays or async functions.
  • Manifest construction in src/cli/index.ts tags remote entries with externalUrl and maps them to local cache paths.
  • Download phase handled by src/cli/external-images/index.ts, which respects remoteImagesDownloadsDelay and processingConcurrency settings.
  • Optimization phase treats downloaded files identically to local assets, passing them through the Sharp pipeline via getOptimizeResult.
  • Storage location defaults to _next/static/media but is configurable via externalImageDir.

Frequently Asked Questions

How does next-export-optimize-images handle external image failures?

If an external image fails to download (network error, 404, etc.), the externalImagesDownloader throws an error that halts the build process. Unlike local images, remote failures are considered blocking because the optimizer cannot generate the requested output variants without the source bytes. You should validate URLs before build time or wrap the CLI in error-handling logic if you need graceful degradation.

Can I use wildcards or glob patterns in remoteImages?

No, the remoteImages option requires explicit URLs or an async function that returns an array of strings. The library does not perform crawling or glob resolution. If you need to optimize dynamic sets of images, implement the fetching logic inside the async remoteImages function to query your CMS or asset API and return the complete list of URLs at build time.

What image formats are supported for external downloads?

The downloader fetches the raw bytes regardless of format, and the optimization pipeline (via Sharp) handles JPEG, PNG, WebP, AVIF, and GIF. The format of the downloaded file does not constrain the output; you can configure the library to convert remote JPEGs to WebP or AVIF during optimization using the standard format options in export-images.config.js.

Is there a cache for downloaded external images between builds?

Yes, downloaded external images are written to the directory specified by externalImageDir (default '_next/static/media'). If this directory persists between builds (for example, if you cache the .next folder in CI), the CLI will skip re-downloading images that already exist locally unless the manifest entries change. This behavior mirrors how local images in public/ are treated during incremental builds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →