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).*)',
  ],
};

I checked your website and it seems to be working fine. Have you resolved this?