image icon indicating copy to clipboard operation
image copied to clipboard

Netlify Provider Does Not Apply `domains` Config to Image CDN*

Open ABB65 opened this issue 9 months ago β€’ 1 comments

Describe the bug

When using the netlify provider in Nuxt Image, the domains array defined in nuxt.config.ts is not automatically applied to Netlify’s image settings.

This results in 400 Bad Request errors for remote images because Netlify blocks external sources unless explicitly allowed in netlify.toml.

Steps to Reproduce

  1. Use the Netlify provider in Nuxt Image and define allowed domains:
    export default defineNuxtConfig({
      image: {
        provider: "netlify",
        netlify: {
          domains: ["via.placeholder.com", "images.unsplash.com"]
        }
      }
    });
    
  2. Deploy the Nuxt project to Netlify.
  3. Try using a remote image, e.g.:
    <NuxtImg src="https://via.placeholder.com/600x400" alt="Placeholder Image" />
    <NuxtImg src="https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=600&q=80" alt="Unsplash Example" />
    
  4. The request to Netlify Image CDN fails with 400 Bad Request.

Expected Behavior

  • The domains specified in nuxt.config.ts should automatically be passed to Netlify’s remote_images setting.
  • Ideally, during build time, the Nuxt Image module should generate a corresponding [images] block inside netlify.toml (or provide instructions to users).

Actual Behavior

Even though the domains are set in nuxt.config.ts, Netlify still blocks the images unless netlify.toml is manually updated.

Workaround

Manually add the allowed domains to netlify.toml:

[images]
    remote_images = ["https://via.placeholder.com/.*", "https://images.unsplash.com/.*"]

However, this approach is not ideal because developers expect the Nuxt Image module to handle the configuration automatically.

Proposed Fix

  • Ensure that domains from nuxt.config.ts is applied to Netlify’s Image CDN settings at build time.
  • Add a warning/log message if domains is defined in nuxt.config.ts but not reflected in netlify.toml.
  • Consider providing an auto-configuration option where Nuxt Image modifies netlify.toml (if permitted).

Environment

  • Nuxt: 3.x
  • Nuxt Image: Latest
  • Node.js: 18.19.1
  • Netlify: Deployed on Production

Additional Context

This issue is causing confusion for developers who expect the Netlify provider to work without additional manual configuration. Addressing this would improve the developer experience and reduce setup complexity.

Would love to hear thoughts from the maintainers on the best approach! πŸš€

ABB65 avatar Mar 19 '25 16:03 ABB65

Hello @ABB65,

I think there is a syntax problem in your nuxt.config.ts file. The domains property should be nested directly under the image object, not inside a netlify object as described in the documentation.

export default defineNuxtConfig({
  image: {
    provider: "netlify",
-   netlify: {
    domains: ["via.placeholder.com", "images.unsplash.com"]
-   }
  }
});
Image

But still, manually adding domains is quite painful, so I built a small script that I'm sharing here.

It automatically scans your project files ({vue,ts,md,yml,yaml} files under the content and app folders, feel free to adapt to your project), finds all unique external domains, and updates your nuxt.config.ts file.

  1. Add the Script to Your Project First, create a scripts directory in the root of your project and place the script (below) inside it.
your-nuxt-project/
β”œβ”€β”€ scripts/
β”‚   └── image-domains.ts
β”œβ”€β”€ nuxt.config.ts
β”œβ”€β”€ package.json
└── ...
  1. Install the Required Library The script uses glob to find your project files and you'll need tsx to run the TypeScript script directly.
npm i -D glob tsx
# or
yarn add -D glob tsx
# or
pnpm add -D glob tsx
  1. Update Your package.json Next, add a few scripts to your package.json file.
{
  "scripts": {
    "build": "tsx scripts/image-domains.ts && nuxt build",
    "build:local": "nuxt build",
    "img": "tsx scripts/image-domains.ts"
  },
}

The script

// scripts/image-domains.ts
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import process from 'node:process'
import { glob } from 'glob'

const nuxtConfigPath = resolve(process.cwd(), 'nuxt.config.ts')
const externalUrlRegex = /https?:\/\/[^"')\s`]+/gi

/**
 * Scans project files to find all unique external domains.
 */
async function fetchDomainsFromProject(): Promise<string[]> {
  console.log('πŸ” Searching for external domains in project files...')
  const files = await glob('{content,app}/**/*.{vue,ts,md,yml,yaml}')
  const domains = new Set<string>()

  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const matches = content.matchAll(externalUrlRegex)
    for (const match of matches) {
      try {
        const url = new URL(match[0])
        domains.add(url.hostname)
      }
      catch {
        // Silently ignore invalid URLs found by the regex
      }
    }
  }

  const domainList = Array.from(domains)
  console.log(`βœ… Found ${domainList.length} unique domains in project.`)
  return domainList
}

/**
 * Extracts the list of currently configured domains from nuxt.config.ts.
 */
function getDomainsFromNuxtConfig(configContent: string): string[] {
  const domainsRegex = /domains:\s*\[([^\]]*)\]/
  const match = configContent.match(domainsRegex)

  if (!match || !match[1]) {
    return [] // No domains array found
  }

  // Extract domains, remove quotes, and trim whitespace
  return match[1]
    .split(',')
    .map(d => d.trim().replace(/['"`]/g, ''))
    .filter(Boolean) // Remove any empty strings from trailing commas etc.
}

async function updateNuxtConfig() {
  const foundDomains = await fetchDomainsFromProject()
  const configContent = readFileSync(nuxtConfigPath, 'utf-8')
  const currentDomains = getDomainsFromNuxtConfig(configContent)

  const foundDomainsSet = new Set(foundDomains)
  const currentDomainsSet = new Set(currentDomains)

  // Determine what has changed
  const domainsToAdd = foundDomains.filter(d => !currentDomainsSet.has(d))
  const domainsToRemove = currentDomains.filter(d => !foundDomainsSet.has(d))

  if (domainsToAdd.length === 0 && domainsToRemove.length === 0) {
    console.log('πŸ‘ Configuration is already up-to-date. No changes needed.')
    return
  }

  // --- Apply Changes ---
  console.log('πŸ”§ Updating nuxt.config.ts...')
  if (domainsToAdd.length > 0)
    console.log(`βž• Added: ${domainsToAdd.join(', ')}`)

  if (domainsToRemove.length > 0)
    console.log(`βž– Removed: ${domainsToRemove.join(', ')}`)

  let newConfigContent = configContent
  const newDomainsString = foundDomains.map(d => `'${d}'`).join(', ')
  const configDomainsRegex = /(image:[\s\S]*?domains:\s*\[)([^\]]*)(\])/

  if (configDomainsRegex.test(newConfigContent)) {
    // Replace the existing domains list
    newConfigContent = newConfigContent.replace(configDomainsRegex, `$1${newDomainsString}$3`)
  }
  else {
    // Add the image config block if it doesn't exist
    newConfigContent = newConfigContent.replace(
      /(defineNuxtConfig\(\{)/,
      `$1
  image: {
    domains: [${newDomainsString}],
  },`,
    )
  }

  writeFileSync(nuxtConfigPath, newConfigContent, 'utf-8')
  console.log('βœ… Successfully saved changes to nuxt.config.ts.')
}

updateNuxtConfig().catch((error) => {
  console.error('❌ Error updating Nuxt configuration:', error)
  process.exit(1)
})

Netlify Deploy Logs

Image

BenjaminOddou avatar Aug 18 '25 14:08 BenjaminOddou