A prebuilt static site is published from a GitHub Pages branch, and every asset under an _assets folder returns 404 on the live site. What is causing it, and what fixes it?
answer
- a branch source is not a file copy
- something eats the underscore folders
- the site's own generator has reserved names
- one empty dotfile at the source root
- .nojekyll disables Jekyll processing
basics
~20 sA branch publishing source is run through GitHub's built-in Jekyll processor, which skips files and folders whose names begin with an underscore, so they never reach the served site. Committing an empty .nojekyll file at the publishing source's root disables that processing.
solid answer
~50 sWhen the publishing source is a branch, GitHub does not just copy your files — it runs Jekyll over them first and serves the generated output. Jekyll treats underscore-prefixed paths as its own source directories (`_layouts`, `_includes`, `_posts`) and does not copy them into the built site, so a generator that emits `_assets` or `_next` produces HTML that references files Pages never publishes. The fix is one empty file: commit `.nojekyll` at the root of the publishing source (the branch's `/` or `/docs`, matching your setting), which tells Pages to skip Jekyll and serve the directory verbatim. Two related symptoms come from the same processor: a build that fails because Markdown or HTML contains `{{ ... }}` that Liquid tries to evaluate, and a site whose files are silently reorganized. Publishing a prebuilt directory as an artifact through a Pages workflow avoids the whole class of problem, because artifact contents are served as uploaded.
code
bash · 6 lines# Publishing source = branch `main`, folder `/docs`
# Build output lives in docs/ and contains docs/_assets/
touch docs/.nojekyll
git add -f docs/.nojekyll
git commit -m "Serve docs/ verbatim on GitHub Pages"
git push origin maingo deeper
Remember the symptom-to-cause link: assets in an underscore folder 404 on a Pages branch site because Jekyll skipped them, and an empty .nojekyll file at the publishing source root fixes it.
Explain the mechanism — a branch source is run through Jekyll, which treats underscore-prefixed paths as build inputs and excludes them from the generated site — and note the sibling symptom of Liquid errors on {{ }} in documentation.
Debug it from evidence rather than memory: check the failed Pages build log, confirm which folder is configured, verify the file is committed and not wiped by a force-pushed deploy, and recommend moving to the artifact publishing source so no second generator is in the path.
Set the org-wide default: prebuilt sites publish via an Actions artifact, Jekyll sites publish from a branch, and nobody mixes the two. That single rule removes an entire class of support ticket across every docs repo you own.
## The hidden build step The surprise here is that "deploy from a branch" is not a static file server pointed at your branch. GitHub runs the branch content through **Jekyll**, the Ruby static site generator that Pages was originally built around, and serves Jekyll's *output*. For a Jekyll site that is exactly what you want. For a directory produced by some other generator, it is an uninvited second build. ## Why underscores disappear Jekyll reserves underscore-prefixed directories for its own inputs — `_config.yml`, `_layouts/`, `_includes/`, `_posts/`, `_data/`, `_sass/`. They are sources, not output, so Jekyll deliberately does not copy them into the generated `_site` directory. It applies the rule by prefix, not by whitelist: any top-level path starting with `_` is treated as private to the build and excluded from the result. Modern JavaScript build tools collide with this constantly, because several of them emit exactly such a directory. A Next.js static export writes its JavaScript and CSS under `_next/`; other toolchains emit `_assets/` or `_app/`. The HTML that references them is copied fine, so the page loads, renders unstyled and inert, and every script and stylesheet request comes back 404. The failure looks like a broken CDN or a bad base path, which is why people burn an afternoon on it. The tell is that the missing paths all begin with an underscore, and that fetching one directly in the browser gives the Pages 404 page rather than a network error. ## The fix Commit an empty file named `.nojekyll` at the **root of the publishing source**. If the source is the branch root, it goes at the branch root; if the source is `/docs`, it goes in `docs/.nojekyll`. Its presence tells Pages to bypass Jekyll and publish the directory as-is, underscores included. ```bash touch .nojekyll # or: touch docs/.nojekyll git add -f .nojekyll # -f because some .gitignore rules skip dotfiles git commit -m "Disable Jekyll processing for Pages" ``` The `-f` detail matters more than it looks: if the file is produced by a build into an output directory that is git-ignored, or a generator wipes the branch on each deploy, `.nojekyll` has to be re-created by that build every time — several generators have a config flag or a plugin that does precisely this. ## The other two symptoms of the same cause **Liquid errors.** Jekyll processes files through the Liquid template language, whose delimiters are `{{ }}` and `{% %}`. Documentation that shows a Vue, Angular, Handlebars, Go or Jinja template — or a code sample containing `{{ user.name }}` — makes the Pages build fail or mangles the output. Inside a real Jekyll site the escape is the `raw` tag; outside one, `.nojekyll` removes the problem entirely. **Silent restructuring.** Files Jekyll considers sources may be relocated or rendered rather than copied, so what is served does not match what you committed. Once you are shipping build output rather than Jekyll sources, that is never what you want. ## The structural fix If your site is built by a tool that is not Jekyll, the cleaner answer is to stop using a branch source at all. Set the publishing source to GitHub Actions, build in a workflow, and hand the output directory to `actions/upload-pages-artifact`; the artifact is served as uploaded, with no second generator in the path. That also removes the constraint that GitHub's Jekyll environment supports only an allowlisted plugin set. If the site genuinely *is* Jekyll, do the opposite: leave Jekyll on and use `_config.yml` to control it. Note that `.nojekyll` and a Jekyll site are mutually exclusive by design — adding the file to a Jekyll source branch publishes raw Markdown instead of a website, which is the same bug in the other direction. ## Debugging checklist 1. Are the 404-ing paths underscore-prefixed? If yes, this is almost certainly it. 2. Is the publishing source a branch (not Actions)? Jekyll only runs on the branch path. 3. Is `.nojekyll` at the root of the *configured folder*, and is it actually committed rather than ignored? 4. Does the deploy process recreate the branch each time, wiping the file? 5. If the Pages build itself failed, the repository shows the failed Pages build and its error — a Liquid syntax error there points at the same processor.
- Does .nojekyll do anything when the publishing source is GitHub Actions?Effectively no. The artifact uploaded by `actions/upload-pages-artifact` is served as it was packaged, so there is no Jekyll pass to disable. Keeping the file in the output directory is harmless and is a reasonable habit if the same directory might later be published from a branch, but it is not what makes underscore paths work in the artifact flow.
- Your Pages build fails with a Liquid syntax error in a Markdown file full of template examples. What are your options?Either stop Jekyll processing the repository at all with `.nojekyll`, or, if the site really is Jekyll, wrap the offending sample in Liquid's `raw`/`endraw` tags so the delimiters are emitted literally. Excluding the file in `_config.yml` also works but removes it from the site.
- The .nojekyll file keeps vanishing from the gh-pages branch. Why?Because the deploy step recreates that branch from a build output directory that does not contain it — a force-push of `dist/` replaces everything. Fix it at the source: have the build emit `.nojekyll` into the output directory, or use a publishing tool that adds it, rather than committing it by hand each time.
saying these in an interview costs you the question
- Blaming a wrong base path or CDN for the 404s
- Thinking Pages serves branch files byte-for-byte
- Putting .nojekyll at the repo root when the source is /docs
- Adding .nojekyll to a real Jekyll site and publishing raw Markdown
- Assuming a git-ignored dotfile was committed