Application Development

Build Succeeds But Site Shows 404: Fix the 7 Root Causes

J
James Eriksson
··12 min read
Fix build succeeds but site shows 404 errors. Diagnose with the Two-URL Test, check framework preset, output directory, SPA fallback, dynamic routes, and DNS. Step-by-step solutions.
TL;DR
  • Build succeeds because it compiles code; routing metadata is separate and configured separately by the deploy platform
  • Run the Two-URL Test: check if your platform default URL (e.g., yourapp.vercel.app) loads without 404; if yes, it is a DNS issue; if no, it is routing metadata
  • Framework preset is the #1 cause; verify it is set to your actual framework (Next.js, React, Svelte, etc.) in your platform settings
  • SPA apps (React, Vite) need a rewrite rule (_redirects or vercel.json) to serve index.html for all routes
  • If you built with Lovable / TanStack Start, verify the Nitro adapter preset is set to "vercel" in your config

Your build succeeded. Your deployment shows "Ready". But every URL returns 404. The code is fine--your routing metadata is wrong. The deploy platform doesn't know which URLs your app serves because you didn't tell it. Fix this in 10 minutes using the diagnostic method in this guide.

Why "Build Succeeds" But "Site Shows 404" Happens

Most developers assume a successful build means the app is ready to serve. It does not. A build compiles your code. It does not configure routing. The deploy platform--Vercel, Netlify, Cloudflare Pages, Ship, or another host--needs separate metadata to know which files answer which requests.

When you deploy, the platform reads your framework preset, build settings, redirects file, and environment variables. It uses that metadata to route requests. If the metadata is wrong or missing, requests hit 404 even if your code compiles.

This is a metadata problem, not a code problem. Your source code is fine. The deploy platform simply doesn't have the instructions it needs. The good news: you can diagnose and fix this without touching your code.

The platform routes by metadata, not by filesystem. Traditional web servers (Apache, Nginx) read a directory of files: public/index.html, public/about.html, public/contact.html. Requests map directly to files. Platforms like Vercel route by metadata: you tell Vercel your framework is React, it generates routes from your source code and node_modules, and requests match against those metadata routes. If metadata is wrong, routes don't exist--even though the code is there.

The Two-URL Test: Diagnose in 30 Seconds

Before you fix anything, run this test. It tells you whether the problem is routing metadata or DNS.

First, find your platform default URL. In Vercel it is yourapp.vercel.app. In Netlify it is yourapp.netlify.app. In Cloudflare Pages it is yourapp.pages.dev. Test it: open the root URL in a browser. Does it load without 404?

If yes, your routing is correct. The 404 is DNS: your custom domain is not pointing to the platform correctly.

If no, your routing metadata is wrong. The platform default URL also returns 404. This means either framework detection failed, output directory is wrong, SPA fallback is missing, dynamic routes are unconfigured, or environment variables are missing.

This one test cuts through 80 percent of guessing. Use it before trying any fix.

Fix #1: Check Your Framework Preset

This is the most common cause. Vercel, Netlify, and Cloudflare Pages auto-detect your framework. They read package.json, look for known dependencies (React, Next.js, Svelte, Vue, etc.), and set the preset automatically. If detection fails, routing breaks.

In Vercel: go to Project Settings, go to Build & Development Settings, check Framework. If it says "Other", auto-detection failed. Change it to your framework name.

In Netlify: go to Build settings, check Build command and Base directory. If Build command is blank or says npm run build without a specific output, detection failed. Set Build command to your actual build command: npm run build, yarn build, pnpm build. Set Publish directory to your output folder: dist, build, .next, out (depends on framework).

In Cloudflare Pages: click Build settings, check Build command and Build output directory. Set both explicitly if they are blank.

After you change the framework preset, redeploy. Wait for build to finish. Test the root URL again.

If it still shows 404, move to Fix #2.

Fix #2: Verify Output Directory & Build Settings

Your framework compiles code into an output folder. If the platform deploys the wrong folder or the build command is wrong, the output folder is empty or doesn't exist.

Run your build locally first. Open a terminal in your project root. Run npm run build (or yarn build, pnpm build). It completes without error. Check your project root directory. You should see a new folder: dist, build, .next, out, or another name depending on your framework.

List the contents. You should see index.html or package.json (for Node.js apps). If the folder is empty, the build is broken. Fix your build locally before deploying.

If the output folder exists locally, the platform must deploy the same folder. In Vercel, this is automatic if framework preset is correct. In Netlify, go to Build settings, find Publish directory, and enter the exact folder name: dist, not ./dist or /dist. In Cloudflare Pages, set Build output directory to the exact name.

Vercel also offers a way to test locally: install Vercel CLI with npm install -g vercel. In your project root, run vercel build. Vercel builds your app the same way it builds on deploy. Check the .vercel/output folder. If it is empty or has no static subfolder, something is wrong with your build.

If the output folder looks right, move to Fix #3 (if you have a React or Vite SPA) or Fix #4 (if you have Next.js).

Fix #3: Add SPA Fallback for React, Vite, and Client-Only Apps

Single-page apps (SPAs) like React Router or Vite apps render routes in the browser, not on the server. They need all requests to return index.html, so the JavaScript can take over and render the right route.

Without SPA fallback, requests to /about look for an about.html file. It doesn't exist, so 404.

To fix it, you need a rewrite rule: tell the platform to serve index.html for any request that doesn't match a static file.

In Vercel, create or edit vercel.json in your project root:

{
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

In Netlify, create or edit _redirects file in your project root (in the public folder, so it deploys with your build output):

/* /index.html 200

The 200 tells Netlify to rewrite (not redirect) the request. The browser sees the URL you typed, but the server serves index.html.

In Cloudflare Pages, create _redirects the same way as Netlify.

After you add the rewrite, rebuild and redeploy. Test /about, /contact, any route. All should load without 404.

Do not add SPA fallback if you use Next.js, Remix, SvelteKit, or Astro. These frameworks handle routing server-side. SPA fallback will break them. Move to Fix #4 instead.

Fix #4: Configure Dynamic Routes in Next.js

Next.js dynamic routes like [id].js or [slug].tsx are not compiled into static files at build time by default. They exist in your code, but the platform does not know they are routable.

You have two options: generate static routes (faster, fewer requests) or allow dynamic generation (slower, any slug works).

For static routes (e.g., product pages for a known set of products), add generateStaticParams in your route file:

export async function generateStaticParams() {
  const products = await fetchProducts();
  return products.map((product) => ({
    slug: product.slug
  }));
}

This tells Next.js to precompile routes for each product slug. At build time, Next.js fetches the product list and creates static files for /products/shoes, /products/shirt, etc.

For dynamic routes (any slug accepted), add this to your route file:

export const dynamic = 'force-dynamic';

This tells Next.js not to precompile the route. At request time, the platform (Vercel, Netlify, Ship) runs a Node.js function to render the page for any slug.

After you add either option, rebuild and redeploy. Test your dynamic routes. They should load without 404.

Fix #5: Verify DNS and Custom Domain Configuration

If your platform default URL (e.g., yourapp.vercel.app) works but your custom domain (e.g., example.com) shows 404, the problem is DNS or domain configuration, not routing metadata.

First, verify the platform detects your domain. In Vercel: go to Domains, check that your domain is listed and shows "Valid Domains". In Netlify: go to Custom domains, check that the domain is listed. In Cloudflare Pages: go to Custom domains.

If the domain is listed, check the DNS record. Go to your domain registrar (GoDaddy, Namecheap, Route 53, Cloudflare, or wherever you bought the domain). Find DNS settings. Look for a CNAME or A record that points to the platform.

For Vercel: CNAME should point to cname.vercel.com. For Netlify: CNAME should point to your-site.netlify.com. For Cloudflare Pages: usually no CNAME needed if your domain is already on Cloudflare.

If the CNAME record points to the wrong place, update it. After you save, wait 5 to 30 minutes for DNS to propagate. The registrar usually shows a TTL (time to live), e.g., 3600 seconds. After that time, the old record expires and the new one takes over.

Some platforms also require SSL verification. You may see an option in your domain settings to "verify SSL". Click it. The platform will generate a DNS challenge. Add that record to your registrar's DNS settings. Wait for verification to complete.

After DNS propagates, test your custom domain. It should load without 404.

Lovable / TanStack Start: The Nitro Adapter Requirement

Lovable recently switched from a Vite SPA to TanStack Start, a server-side rendering framework. Old SPA deployment tricks no longer work.

If you built a Lovable app and deployed it to Vercel, Netlify, or Ship, and all routes except the root show 404, you probably hit this issue.

The fix: Lovable must use the Nitro adapter and set preset: "vercel" in its config. Without the Nitro adapter, TanStack Start has nowhere to run the server-side rendering logic, so routes fail.

Open your Lovable project. Find preview.config.ts (or nitro.config.ts). Check if you have this:

export default definePreviewConfig({
  nitro: {
    presets: ['vercel']
  }
});

If it is missing or says presets: [], add the Vercel preset. Save, commit to git, push, and redeploy.

After redeployment, test your routes. They should load without 404.

If your Lovable project is an older one that still uses Vite instead of TanStack Start, the SPA fallback method (Fix #3) will work instead.

When to Stop Troubleshooting: Managed Hosting vs. DIY

If you have tried all five fixes and routes still show 404, you face a choice: keep debugging or switch platforms.

Vercel and Netlify are free to start but require careful configuration. Each fix takes 5 to 15 minutes, and finding the right one means trial and error. If you are new to deployment, this can take 2 to 4 hours.

Hetzner with Coolify is the cheapest DIY option. You run your own Kubernetes cluster on Hetzner's affordable VPS. Coolify handles deployment. Cost: 5 EUR to 50 EUR per month, depending on VM size. Trade-off: you manage infrastructure. Coolify handles deployment, but if something breaks, you debug it yourself.

Managed hosting like Ship eliminates routing config entirely. Ship auto-detects your framework from your code, auto-configures routing, and auto-handles DNS and SSL. Cost: flat rate per app, typically 20 to 50 USD per month. Trade-off: less flexibility. Ship runs on a fixed stack.

For a 10-person team evaluating hosting, Ship's flat-rate model and routing certainty save debugging time. You commit code, it deploys, routing works. This certainty has value when your time costs more than the hosting cost.

However, if raw cost is the constraint and you have time to debug, Hetzner and Coolify win. If you need a free tier to validate an idea, Vercel and Netlify win. If you need routing to just work, Ship or another managed platform wins.

Choose based on your constraint: time, cost, or flexibility.

Frequently Asked Questions

Why does the build log say "Ready" if routing is broken?

Build and deployment are separate steps. Build compiles your code. The platform says "Ready" when your code compiles and uploads successfully. It does not validate routing until a request comes in. If routing metadata is wrong, the code uploads fine, build shows "Ready", but requests 404.

Why does it work locally but not after deploy?

Local development uses a dev server (e.g., npm start). The dev server watches your source code and recompiles on every change. It serves index.html by default for any route. When you build for production (npm run build) and deploy the output folder, the dev server is gone. The platform becomes your server. It must be told what to serve.

How do I know which framework preset to use?

Most platforms auto-detect. Go to Project Settings and look at Framework. It should show your framework name: Next.js, React, Svelte, Vue, Remix, etc. If it says "Other", select your framework from the dropdown.

Can I have SPA fallback AND Next.js dynamic routes?

No. SPA fallback rewrites all requests to index.html. This breaks Next.js server-side rendering. Use SPA fallback only for client-only SPAs (React Router, Vite, Create React App). Use dynamic route configuration only for Next.js, Remix, or other server-side frameworks.

What if my environment variables are missing?

Some apps fail to render if environment variables are missing. If routes show 404 and everything else is configured, check your platform's environment variable settings. In Vercel: Project Settings, Environment Variables. In Netlify: Build & Deploy, Environment. Add every variable your app needs. Redeploy.

Does this apply to all frameworks?

Yes, but the fix varies. React and Vite need SPA fallback. Next.js, Remix, and SvelteKit need dynamic route config. Astro auto-handles static routes. The root cause is always the same: routing metadata is missing or wrong.

How long does DNS propagation take?

Typically 5 to 30 minutes. DNS uses caching, and your ISP may cache records. Some registrars set very long TTLs (hours). If you just changed a DNS record, wait at least 10 minutes before assuming it failed.

The Bottom Line

Your build succeeds because it compiles code. Your site shows 404 because you did not configure routing metadata. The platform needs to know your framework, your output directory, and how to handle SPA routes or dynamic routes. Run the Two-URL Test, identify which fix applies, and redeploy. Most developers fix this in under 10 minutes.

If routing configuration repeatedly becomes a blocker, consider managed hosting. When you deploy to Ship, routing auto-configures itself based on your code--no metadata, no guessing, no debugging. Start hosting your app on Ship and let framework detection handle the rest.

Routing configured automatically
Ship detects your framework and routing from your code, no config files needed.
Get Started Free

Ready to self-host your own apps?

One server. Multiple apps. No per-app fees.

Get started →