Next.JS wesbite - CSS Not Rendering on Prod & Static Assets Not Deploying (404 Errors)

Netlify Support Request: Static Assets Not Deploying (404 Errors)

Problem Summary

Site URL: https://perfectlyhired.com
Issue: All _next/static/* assets (CSS, JavaScript, fonts) return 404 errors on production deployment, while the site works correctly locally and on Netlify preview deployments.

Symptom: Deployment logs show only “4 new file(s) to upload” when there should be 50+ static files (55 files verified locally: 46 JS files, 1 CSS file, 7 font files, plus manifest files).

Impact: Site renders without any styling, JavaScript functionality, or fonts on production domain.


Technical Details

Build Configuration

  • Next.js Version: 16.0.10
  • @netlify**/plugin-nextjs Version**: 5.15.2 (latest)
  • Node Version: 20.12.2
  • Build Mode: SSR (Server-Side Rendering, NOT static export)
  • Build Command: npm run generate-sitemap && npm run build

Local Build Verification :white_check_mark:

  • Static Assets Generated: 55 files in .next/static/
    • 46 JavaScript files in chunks/
    • 1 CSS file: chunks/c4026e3d31803ba7.css
    • 7 font files in media/
    • Manifest files (_buildManifest.js, _ssgManifest.js, etc.)
  • Total Size: ~1.35 MB
  • Build Status: :white_check_mark: Successful

Production Deployment Evidence

From latest deployment logs:

Starting to deploy site from ‘.next’
Calculating files to upload
4 new file(s) to upload :warning: THIS IS THE PROBLEM
4 new function(s) to upload

 **Expected**: 50+ files should be uploaded **Actual**: Only 4 files uploaded ### Example 404 URLs (All Return 404) - `https://perfectlyhired.com/_next/static/chunks/c4026e3d31803ba7.css` - `https://perfectlyhired.com/_next/static/chunks/185c2acb2763bace.js` - `https://perfectlyhired.com/_next/static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2` --- ## Current Configuration ### `netlify.toml` ```toml [build] command = "npm run generate-sitemap && npm run build" publish = ".next" [[plugins]] package = "@netlify/plugin-nextjs" [build.environment] NODE_VERSION = "20.12.2" NETLIFY_NEXT_SKEW_PROTECTION = "true" [functions] node_bundler = "esbuild" external_node_modules = ["node-fetch"] # HTTP Headers for SEO [[headers]] for = "/*" [headers.values] X-Robots-Tag = "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" X-Frame-Options = "DENY" X-Content-Type-Options = "nosniff" # Ensure CSS and JS assets are served correctly [[headers]] for = "/_next/static/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" [[headers]] for = "/_next/static/css/*" [headers.values] Content-Type = "text/css" Cache-Control = "public, max-age=31536000, immutable" 

next.config.js

/** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['tsx', 'ts', 'jsx', 'js'], images: { domains: ['perfectlyhired.com'], }, async redirects() { return [ { source: '/perfectly-hired-ai-powered-role-creation', destination: '/ai-powered-job-description', permanent: true, }, { source: '/locations', destination: '/', permanent: true, }, ]; }, async rewrites() { return [ { source: '/recruitment-service/hire-:slug(.*)', destination: '/recruitment-service/hire/:slug', }, ]; }, async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'X-Robots-Tag', value: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1', }, ], }, ]; }, }; module.exports = nextConfig; 

middleware.ts

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const SKIP_PREFIXES = [ '/_next', '/api', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/icons', '/images', ]; export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; if (SKIP_PREFIXES.some(prefix => pathname.startsWith(prefix))) { return NextResponse.next(); } return NextResponse.next(); } export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], }; 

public/_redirects

  • Contains 5,000+ specific redirect rules
  • No catch-all redirects (commented out: # /* /index.html 200)
  • No redirects matching /_next/* paths
  • All redirects are specific URL-to-URL mappings

.netlifyignore

# Dependencies node_modules/ # Source files (not needed in deployment) src/ scripts/ archive/ *.backup *.md !README.md # Config and dev files .git/ .vscode/ .idea/ *.log .env .env.local .env.*.local # Test files **/*.test.* **/*.spec.* # Note: .next/ is needed for @netlify/plugin-nextjs, so we don't exclude it 

Everything We’ve Tried

:white_check_mark: Configuration Changes

  1. Explicit publish directory: Set publish = ".next" in netlify.toml
  2. Removed publish directory: Tried letting plugin handle it automatically
  3. Removed redirects from netlify.toml: Moved all redirects to public/_redirects to avoid conflicts
  4. Updated middleware: Explicitly configured to skip /_next paths
  5. Added skew protection: NETLIFY_NEXT_SKEW_PROTECTION = "true"

:white_check_mark: Plugin Configuration

  1. Verified plugin version: Using latest @netlify/plugin-nextjs@5.15.2
  2. Explicit plugin declaration: Plugin is explicitly listed in netlify.toml and package.json
  3. Removed invalid plugin inputs: Removed incrementalSourceBundling (not supported in v5.15.2)

:white_check_mark: Build Verification

  1. Local build check: Confirmed 55 static assets are generated correctly
  2. Post-build verification script: Created diagnostic script that confirms .next/static/ contains all files
  3. Build logs analysis: Build completes successfully, all files exist in .next/static/

:white_check_mark: Netlify UI Checks

  1. Publish directory: Verified in Netlify UI (set to .next or empty)
  2. Build command: Matches netlify.toml configuration
  3. Environment variables: Verified BUILD_HOOK_URL and GOOGLE_SHEETS_WEBHOOK (unrelated to static assets)

:cross_mark: Attempted Solutions That Didn’t Work

  1. Standalone build mode: Tried output: 'standalone' in next.config.js - Plugin explicitly failed with error: “Your publish directory does not contain expected Next.js build output”
  2. Manual static asset copy: Attempted copying .next/static to public/_next/static - Failed because Next.js reserves /_next route and conflicts with public/_next directory
  3. Removing publish directory: Tried removing publish from netlify.toml - No change, still only 4 files
  4. Adding plugin inputs: Tried incrementalSourceBundling = false - Plugin doesn’t accept this input in v5.15.2

Everything We Suspected (But Wasn’t the Issue)

:cross_mark: Redirects Interference

Suspicion: Redirects in netlify.toml or public/_redirects might be catching /_next/static/* requests
Investigation:

  • Checked all redirects - none match /_next/* patterns
  • Removed redirects from netlify.toml - no change
  • Verified catch-all redirect is commented out
  • Conclusion: Not the issue

:cross_mark: Middleware Interference

Suspicion: Middleware might be intercepting static asset requests
Investigation:

  • Updated middleware to explicitly skip /_next paths
  • Added matcher config to exclude static assets
  • Conclusion: Not the issue (middleware correctly configured)

:cross_mark: Publish Directory Configuration

Suspicion: Wrong publish directory or plugin not finding .next/static
Investigation:

  • Tried explicit publish = ".next"
  • Tried removing publish directory (let plugin handle it)
  • Verified .next/static/ exists with 55 files after build
  • Conclusion: Not the issue (directory exists, plugin should find it)

:cross_mark: Plugin Version

Suspicion: Outdated plugin version might have bugs
Investigation:

  • Currently using 5.15.2 (latest version)
  • Checked npm registry - no newer version available
  • Conclusion: Not the issue (using latest version)

:cross_mark: CSS File Location

Suspicion: CSS files in wrong location (e.g., public/ directory)
Investigation:

  • CSS is correctly located in app/globals.css
  • No CSS modules in public/ directory
  • CSS is properly imported in app/layout.tsx
  • Conclusion: Not the issue (correct setup)

:cross_mark: .gitignore Excluding Files

Suspicion: .next/ in .gitignore might prevent deployment
Investigation:

  • .next/ is correctly gitignored (standard practice)
  • Netlify builds generate .next/ during build process
  • Build logs confirm .next/static/ exists after build
  • Conclusion: Not the issue (normal and expected)

:cross_mark: Environment Variables

Suspicion: Missing or incorrect environment variables
Investigation:

  • NETLIFY_NEXT_SKEW_PROTECTION = "true" is set
  • BUILD_HOOK_URL exists (unrelated to static assets)
  • NODE_VERSION = "20.12.2" is set
  • Conclusion: Not the issue (correctly configured)

:cross_mark: .netlifyignore Excluding Files

Suspicion: .netlifyignore might be excluding .next/static/
Investigation:

  • .netlifyignore explicitly does NOT exclude .next/
  • Comment in file confirms: “Note: .next/ is needed for @netlify/plugin-nextjs”
  • Conclusion: Not the issue

:cross_mark: Next.js Configuration

Suspicion: next.config.js might have incorrect settings
Investigation:

  • No output: 'export' (correct for SSR)
  • No output: 'standalone' (we tried this, plugin doesn’t support it)
  • Standard SSR configuration
  • Conclusion: Not the issue (correct configuration)

:cross_mark: Build Cache Issues

Suspicion: Stale build cache causing issues
Investigation:

  • Cleared Netlify cache multiple times
  • Verified fresh builds generate correct files
  • Conclusion: Not the issue (cache cleared, problem persists)

What We Need Help With

  1. Why is the plugin only uploading 4 files instead of 55+?

    • The build generates all files correctly
    • The plugin should automatically package .next/static/
    • What is the plugin actually seeing/processing?
  2. Is this a known bug with Next.js 16 and plugin v5.15.2?

    • Are there compatibility issues?
    • Are there workarounds or fixes available?
  3. How can we verify what the plugin is processing?

    • Can we get more detailed logs from the plugin?
    • What files is it actually finding in .next/static/?
  4. Is there a configuration we’re missing?

    • Are there required environment variables?
    • Are there plugin inputs we should be using?
  5. Why do preview deployments work but production doesn’t?

    • Same build process
    • Same plugin version
    • Different behavior between preview and production

Additional Information

Deployment Logs (Key Excerpts)

✅ Build completed successfully ✅ "Starting to deploy site from '.next'" ⚠️ "4 new file(s) to upload" - This is suspiciously low! ✅ Functions bundled correctly ✅ Site deployed successfully 

Preview vs Production

  • Preview deployments: Static assets work correctly
  • Production deployment: Static assets return 404
  • Same build process: Identical configuration and build command

Diagnostic Script Output (Local)

✅ Verified 55 static assets exist in .next/static → These should be deployed by Netlify from .next directory 

Additional Files (If Needed)

Here are the configuration files:

Configuration Files

netlify.toml

[build] command = "npm run generate-sitemap && npm run build" # Explicitly set publish directory - plugin will process .next and deploy static assets publish = ".next" [[plugins]] package = "@netlify/plugin-nextjs" [build.environment] NODE_VERSION = "20.12.2" # Enable skew protection to prevent 404s for static assets NETLIFY_NEXT_SKEW_PROTECTION = "true" [functions] node_bundler = "esbuild" external_node_modules = ["node-fetch"] # HTTP Headers for SEO [[headers]] for = "/*" [headers.values] X-Robots-Tag = "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" X-Frame-Options = "DENY" X-Content-Type-Options = "nosniff" # Ensure CSS and JS assets are served correctly [[headers]] for = "/_next/static/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" [[headers]] for = "/_next/static/css/*" [headers.values] Content-Type = "text/css" Cache-Control = "public, max-age=31536000, immutable" # Note: _next/static/* assets are automatically handled by @netlify/plugin-nextjs # Do not add redirects for _next/* as they interfere with the plugin # Redirect blog tag pages to knowledge hub (handled in public/_redirects instead) # Temporarily removed from netlify.toml to test if redirects interfere with static assets # [[redirects]] # from = "/blog/tag/*" # to = "/knowledge-hub" # status = 301 # force = true 

next.config.js

/** @type {import('next').NextConfig} */ const nextConfig = { // Explicitly use App Router only - ignore pages directory pageExtensions: ['tsx', 'ts', 'jsx', 'js'], images: { domains: ['perfectlyhired.com'], }, async redirects() { return [ { source: '/perfectly-hired-ai-powered-role-creation', destination: '/ai-powered-job-description', permanent: true, }, { source: '/locations', destination: '/', permanent: true, }, ]; }, async rewrites() { return [ { source: '/recruitment-service/hire-:slug(.*)', destination: '/recruitment-service/hire/:slug', }, ]; }, async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'X-Robots-Tag', value: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1', }, ], }, ]; }, }; module.exports = nextConfig; 

package.json (Relevant Sections)

{ "name": "perfect-hire-landing-page", "version": "0.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "postbuild": "node scripts/copy-static-assets.js", "start": "next start", "lint": "next lint", "generate-sitemap": "node scripts/generate-sitemap.mjs", "generate-blog-index": "node scripts/generateBlogIndex.js", "diagnose": "node scripts/diagnose-build.js" }, "dependencies": { "@netlify/functions": "^4.1.5", "next": "^16.0.10", "react": "^18.3.1", "react-dom": "^18.3.1", "node-fetch": "^3.3.2" }, "devDependencies": { "@netlify/plugin-nextjs": "^5.15.2", "typescript": "^5.5.3" } } 

middleware.ts

import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // Skip middleware for static assets and API routes const SKIP_PREFIXES = [ '/_next', '/api', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/icons', '/images', ]; export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; // Skip middleware for static assets if (SKIP_PREFIXES.some(prefix => pathname.startsWith(prefix))) { return NextResponse.next(); } // All redirects are handled by Netlify _redirects file return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };