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 
- 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.)
- 46 JavaScript files in
- Total Size: ~1.35 MB
- Build Status:
Successful
Production Deployment Evidence
From latest deployment logs:
Starting to deploy site from ‘.next’
Calculating files to upload
4 new file(s) to upload
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
Configuration Changes
- Explicit publish directory: Set
publish = ".next"innetlify.toml - Removed publish directory: Tried letting plugin handle it automatically
- Removed redirects from netlify.toml: Moved all redirects to
public/_redirectsto avoid conflicts - Updated middleware: Explicitly configured to skip
/_nextpaths - Added skew protection:
NETLIFY_NEXT_SKEW_PROTECTION = "true"
Plugin Configuration
- Verified plugin version: Using latest
@netlify/plugin-nextjs@5.15.2 - Explicit plugin declaration: Plugin is explicitly listed in
netlify.tomlandpackage.json - Removed invalid plugin inputs: Removed
incrementalSourceBundling(not supported in v5.15.2)
Build Verification
- Local build check: Confirmed 55 static assets are generated correctly
- Post-build verification script: Created diagnostic script that confirms
.next/static/contains all files - Build logs analysis: Build completes successfully, all files exist in
.next/static/
Netlify UI Checks
- Publish directory: Verified in Netlify UI (set to
.nextor empty) - Build command: Matches
netlify.tomlconfiguration - Environment variables: Verified
BUILD_HOOK_URLandGOOGLE_SHEETS_WEBHOOK(unrelated to static assets)
Attempted Solutions That Didn’t Work
- Standalone build mode: Tried
output: 'standalone'innext.config.js- Plugin explicitly failed with error: “Your publish directory does not contain expected Next.js build output” - Manual static asset copy: Attempted copying
.next/statictopublic/_next/static- Failed because Next.js reserves/_nextroute and conflicts withpublic/_nextdirectory - Removing publish directory: Tried removing
publishfromnetlify.toml- No change, still only 4 files - Adding plugin inputs: Tried
incrementalSourceBundling = false- Plugin doesn’t accept this input in v5.15.2
Everything We Suspected (But Wasn’t the Issue)
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
Middleware Interference
Suspicion: Middleware might be intercepting static asset requests
Investigation:
- Updated middleware to explicitly skip
/_nextpaths - Added matcher config to exclude static assets
- Conclusion: Not the issue (middleware correctly configured)
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)
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)
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)
.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)
Environment Variables
Suspicion: Missing or incorrect environment variables
Investigation:
NETLIFY_NEXT_SKEW_PROTECTION = "true"is setBUILD_HOOK_URLexists (unrelated to static assets)NODE_VERSION = "20.12.2"is set- Conclusion: Not the issue (correctly configured)
.netlifyignore Excluding Files
Suspicion: .netlifyignore might be excluding .next/static/
Investigation:
.netlifyignoreexplicitly does NOT exclude.next/- Comment in file confirms: “Note: .next/ is needed for @netlify/plugin-nextjs”
- Conclusion: Not the issue
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)
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
-
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?
-
Is this a known bug with Next.js 16 and plugin v5.15.2?
- Are there compatibility issues?
- Are there workarounds or fixes available?
-
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/?
-
Is there a configuration we’re missing?
- Are there required environment variables?
- Are there plugin inputs we should be using?
-
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).*)',
],
};