troubleshooting

Fix "index.html not found" in a website ZIP

By PreviewSend Team · Jul 18, 2026 · 8 min read

An upload can fail outright with a message like "index.html not found." Sometimes the upload completes, but the hosted URL returns a 404 at the root while individual asset files still load. A root 404 can have other causes, but if the platform reports a missing entry file, the first thing to inspect is the ZIP: it probably has no index.html where the host expects one.

This is a diagnosis, not a rebuild-from-scratch. The files are probably fine. Something about the archive's shape or the entry file's name is off, and it is faster to look before you guess. If you are setting up a ZIP for the first time rather than fixing a failed one, the ZIP hosting guide walks through the layout up front.

The fix, first

The usual portable fix is simple: put a file named exactly index.html, in lowercase, at the root of the archive rather than inside a wrapper folder. PreviewSend can also use the shallowest nested copy, but a flat archive avoids relying on host-specific fallback behavior. Open the ZIP, inspect the layout, and rebuild it if needed:

cd path/to/your/site   # the folder that directly contains index.html
zip -r ../site.zip .    # the trailing . zips the contents, not the folder

The rest of this article is how to tell which specific thing went wrong, so the rebuild actually fixes it instead of reproducing the same archive.

Look inside the ZIP without guessing

Don't reason about what the archive contains from memory. List it. On macOS or Linux, unzip -l prints every path without extracting anything:

unzip -l site.zip

Read the paths in the output, not the file sizes. A correct archive has index.html on its own, with no directory in front of it:

  Length      Date    Time    Name
---------  ---------- -----   ----
     2118  07-18-2026 09:14   index.html
      840  07-18-2026 09:14   styles.css
      512  07-18-2026 09:14   app.js
        0  07-18-2026 09:14   images/
    18244  07-18-2026 09:14   images/logo.png

If you'd rather not use the terminal, double-click the ZIP to expand it and look at what lands: you want index.html sitting directly beside styles.css and the asset folders, not one level down inside another folder. Whichever way you look, the output tells you which of the cases below you're in.

No index file anywhere

Search the listing for the name. If nothing matches, the archive genuinely has no entry page:

unzip -l site.zip | grep -i index.html

An empty result means one of two things. Either the file was never named index.html in the first place (covered next), or you zipped a project that hasn't produced one yet (covered below under source versus build output). In MDN's conventional website project layout, index.html is the file that generally contains the homepage. That is the entry page a static host needs for the root URL.

Wrong filename, case, or extension

If grep above found something close but not exact, the name is the problem. These all fail on a host expecting the default entry page:

  • home.html or main.html: a perfectly valid page, just not the name the host serves at /.
  • Index.html or index.HTML: the right word, wrong case. Most web servers are case sensitive, so this is treated as a different, missing file. GitHub Pages spells this out in its 404 troubleshooting docs: the entry file must be at the top level of the published source and the name is case sensitive.
  • index.htm: the older three-letter extension. Some hosts accept it as a default; plenty don't. Rename it to index.html and you stop depending on which.
  • index.html.txt: an editor that appended a real extension to a file you thought was HTML. The Finder or Explorer "hide extensions" setting is usually why you didn't notice.

MDN's rule for filenames is worth adopting: lowercase, no spaces. Servers treat case and spacing literally even when your own machine is forgiving about them. Rename the entry file to index.html and rebuild the archive.

A wrapper directory around everything

This is the most common cause, and the listing makes it obvious. Every path shares the same leading folder:

  Length      Date    Time    Name
---------  ---------- -----   ----
        0  07-18-2026 09:14   my-site/
     2118  07-18-2026 09:14   my-site/index.html
      840  07-18-2026 09:14   my-site/styles.css
    18244  07-18-2026 09:14   my-site/images/logo.png

Here index.html exists, but it's at my-site/index.html, one level below the root. A host that looks for a top-level entry finds the my-site/ folder and nothing to serve at /.

This happens because of how the "Compress" command works on macOS and Windows. Right-click a folder and compress it, and you get an archive that contains that folder. What you want is to open the folder first, select everything inside it, and compress the selection instead. On the command line, cd into the folder so the trailing . captures its contents rather than the folder itself:

cd my-site
zip -r ../site.zip .

PreviewSend is more forgiving here than a plain static host: its parser prefers a root-level index.html and otherwise falls back to the shallowest index.html it can find, so a single wrapper folder still resolves. That's a safety net, not a reason to rely on it. Other hosts won't rescue a nested entry file, so root placement is the portable habit. The plain HTML, CSS, and JavaScript guide shows the same flat layout for a hand-written folder.

Source tree instead of built output

If the listing shows files like these, the archive is a source project, not a hosted site:

  Length      Date    Time    Name
---------  ---------- -----   ----
      480  07-18-2026 09:14   package.json
        0  07-18-2026 09:14   src/
     1204  07-18-2026 09:14   src/App.jsx
     3320  07-18-2026 09:14   src/main.jsx
        0  07-18-2026 09:14   node_modules/

A source project is not deployable output even when it contains a root index.html, as Vite projects usually do. That source entry can still import unbundled files from src/. A React, Vue, Svelte, or similar project turns its source into transformed HTML and bundled assets when you build it. Zipping the project folder captures the ingredients, not the finished page.

Run the build first, then zip the output folder's contents:

npm run build       # or pnpm build / yarn build

The output folder name depends on the tool: dist/ for Vite and Astro, build/ for Create React App, out/ for a Next.js static export. On a Vite project, the dist deployment guide covers the build, preview, and upload steps in order. The tell is a fresh index.html next to a hashed assets/ or _next/ directory, with filenames like index-a1b2c3.js. That folder's contents are what you zip. Do not assume a no-code or AI tool's download is ready to host. If it contains index.html and compiled assets, it is probably output; if it contains source files, follow that tool's build instructions first.

Rebuild the archive with index.html at the root

Once you know which case you had, the rebuild is the same move every time: get into the directory that directly contains index.html, then compress its contents rather than the directory.

cd dist                 # or build, or out, or your plain site folder
zip -r ../site.zip .

The . means "everything in this directory," so index.html lands at the top of site.zip. Without the terminal, open the folder in Finder or Explorer, select all the files inside it, and compress the selection. The result you're after is an archive whose first-level listing includes a bare index.html.

Verify the fixed archive before you retry

Confirm the rebuild worked before uploading, so you're not testing it by trial and error on the host:

unzip -l site.zip | grep -i index.html

Read the path in the match. A bare index.html with no folder segment in front of it means the entry file is at the root. If the line still shows something/index.html, the wrapper remains. PreviewSend can still select the shallowest nested entry, but a stricter host may reject the archive. Two more quick checks worth doing while you're here:

  • Confirm the name is exact and lowercase: index.html, not index.HTML or index.htm.
  • Check asset URLs against where the site will be mounted. Relative references such as href="styles.css" are portable across directories. Root-relative references such as href="/styles.css" are correct when the site owns the domain root, including PreviewSend preview URLs, but can break when another host mounts the site under a subpath. The relative-path section of the ZIP guide covers that in more depth.

Retry the upload

With the entry layout verified, upload the archive again. Hosts that take a prebuilt drop don't run a build for you, so the entry file has to be usable in whatever you hand over: Cloudflare Pages Direct Upload accepts the ZIP or a folder through its dashboard, while Netlify's manual deploy takes the built output folder itself. A misnamed entry fails broadly; acceptance of a nested entry depends on the host.

On PreviewSend, create a project and upload site.zip (up to 100 MB compressed). PreviewSend checks the archive's size and safety limits, selects a root or shallowest nested index.html, extracts the files, and makes the successful upload active at the project's stable URL. Eligible first-time owners get a one-time 14-day Pro trial on their first workspace without entering a card; afterward, Free limits apply. New projects enable account-free client review by default, so clients can pin feedback on the corrected build. If validation still fails, inspect the listing again: your terminal shows raw archive names, while PreviewSend normalizes separators and path segments before selecting the entry.

FAQ

Why does my upload say "index.html not found" when the file is clearly in my folder?

Many hosts look for index.html at the top level of the archive. If you compressed the enclosing folder, the entry file ends up at my-site/index.html instead of index.html, so a strict host sees a wrapper directory and no root entry. List the archive with unzip -l to confirm where the file actually sits.

Does the entry file have to be spelled exactly index.html?

For broad compatibility, use the exact lowercase name index.html. PreviewSend detects case variants, but many hosts are case-sensitive; index.htm is a separate filename and is not universally recognized. GitHub Pages also accepts index.md or README.md.

I zipped the whole project and it still fails. What did I miss?

You probably zipped the source tree instead of the build output. A source project can contain index.html, as Vite projects usually do, but that entry may still import unbundled files from src/. Run the production build, then zip the contents of its generated output folder, such as dist/, build/, or out/.

How do I check the archive is correct before uploading again?

Run unzip -l site.zip and inspect the paths. A bare index.html is the most portable layout. If every path starts with the same folder name, the wrapper remains; PreviewSend can use the shallowest nested index.html, but stricter hosts may reject it.

Have a site to host? Upload it and get a live link in seconds.

Get it free

← Back to the blog