Development notes
A 404 page is more than an error message: handling unknown URLs on a static site
Use recorded HTTP responses, error-page HTML, and multilingual fallback behavior to distinguish generating 404.html, returning a 404, and helping readers continue browsing, while keeping local preview checks separate from production-host verification.
- Astro
- HTTP
- Static sites
- Multilingual sites
Opening 404.html directly and seeing the message 「頁面不存在」 (“Page not found”) does not prove that a site handles unknown URLs correctly.
In the local static preview checks for the original note, I confirmed two different responses: requesting a URL that did not exist returned HTTP 404, while requesting 404.html directly returned HTTP 200. Both could display the same error page, but only the first checked how the site handled an unknown URL.
I check 404 handling as two separate responsibilities: the server must report honestly that the URL has no content, and the page must explain what happened and give readers somewhere to go next. An attractive error illustration, clear copy, and the correct status code each have their own job.
Test a URL that does not exist, not the error-page file
Astro can generate a custom error page from 404.astro or 404.md, producing 404.html in a static build. Its documentation also notes that most deployment services will find and use this file. That does not mean every server that serves HTML follows the same rules.[1]
There are two layers of responsibility here: the build tool produces a usable document, while the server hosting the site decides which requests should use it and which status code to return.
At a minimum, the following requests need separate checks:
| Request | What it checks |
|---|---|
Open 404.html directly |
Whether the error-page file exists and its content and assets can be read |
| Open a URL confirmed not to exist | Whether the server serves the page as an error response rather than normal content or the homepage |
The 200 returned when opening the error-page file directly was an observed file-access behavior in those local preview checks. It was not evidence that unknown-URL handling had failed. The second request is the one to use for that judgment, and both its status code and response body need to be read.
What sending every URL to the homepage can hide
For this static site, which already generates HTML for each page, there is no need to rewrite every unknown path to the homepage and leave the browser to decide what to display. The deployment configuration inspected for those checks specified Astro and the static output directory without adding a site-wide rewrite of that kind.
Suppose someone mistypes an article URL, but the server still returns the homepage with HTTP 200. A test might record only that the site responded and could be opened. The article the reader was looking for has not appeared.
Another possibility is to keep the error message while still returning 200. Google’s documentation explains that a successful status code paired with an empty page or error content may be classified by its search systems as a soft 404. The three digits “404” on the page cannot replace the HTTP status.[2]
Not every old URL should be treated as an error, though. This site’s /work is an intentionally retained compatibility entry point. In the recorded local preview checks, it returned 200 with readable work content and a canonical pointing to /about/work. That is a different URL contract from an arbitrary path that does not exist.
Before testing, distinguish a primary page, a compatibility entry point, and an unknown URL. Otherwise, intentionally retained content can be mistaken for a failure to return 404.
noindex controls indexing, not the status code
The error page retained a title, description, and readable body, but did not reuse the homepage’s canonical, og:url, alternate-language metadata, or structured data. The error page was not included in the sitemap either. The navigation still let readers leave the error page without marking the homepage as the normal page they were currently viewing.
This is a choice about page identity: an error page can use the shared site shell without claiming that an unknown URL is another version of the homepage.
The inspected HTML also included noindex. This is an indexing directive; it does not automatically turn HTTP 200 into 404. Google has separate handling rules for URLs that actually return 4xx responses, so these should not be treated as the same check.[2][3]
Google also explains that a crawler must be able to access a page to read noindex; robots.txt must not block it first.[3] Finding the directive in the head confirms only that the site emitted it. It does not establish that search results have already changed.
For the broader relationship between primary pages, compatibility pages, and public metadata, see How static HTML, canonical, hreflang, and sitemaps work together. This note stays focused on the response an unknown URL actually receives.
One shared error page limits the initial language
The implementation built one shared error page whose initial HTML was in Traditional Chinese. A browser script then read the first URL segment: en selected English, ja selected Japanese, and anything else selected Traditional Chinese. It updated the title, description, lang, navigation labels, and homepage link.
In the recorded requests to unknown English and Japanese URLs, both HTTP responses were 404, and both raw HTML responses were still in Traditional Chinese. After the browser ran the script, the English page’s h1 became Page not found, with its homepage link pointing to /en. The Japanese page showed ページが見つかりません, with its link pointing to /ja. The URL did not change.
The supported conclusion is that a browser script adds language-specific content to a shared error page, not that the server returns error HTML in the corresponding language from the outset.
When JavaScript does not run, the Traditional Chinese error explanation remains, and the page also provides homepage links for all three languages. This preserves ways to leave the error page, but it is not a complete English or Japanese error page without scripting. If each language later needs correct initial HTML, the design must also account for how the host selects an error document. Adding a page under a language directory alone does not prove that the host will use it for unknown URLs.
Helping readers leave does not require rewriting their URL first
Offering a homepage link and automatically sending readers to the homepage differ in whether they preserve the original request.
In the recorded checks, unknown-URL responses did not first redirect to an error-page URL that returned a successful response. The browser stayed on the original path, so readers could check the spelling, copy the broken link, or choose to go home. The homepage and navigation controls were native links; they did not need to wait for another action handler to decide their destination.
The error page’s language switch also preserved the logical path of the missing content. Switching an unknown English URL to Japanese, for example, changed the language prefix rather than presenting nonexistent content as a successful page. This was the script’s responsibility in the implementation inspected then; it did not mean switching languages would find the article.
I would make the recovery options more definite than the explanation of the error: ask readers to check the URL or offer a homepage that actually exists. Without evidence, I would not assert that “the article has been deleted” or add search or contact features that are not available.
Local preview had a trailing-slash exception
The site configuration used trailingSlash: 'never'. When I tested an unknown path without a trailing slash, local preview used the site’s custom error page. Adding / to the same path still returned 404, but the response body was Astro’s trailing-slash error page rather than the site’s own page.
That difference shows why checking the status code alone is not enough. Both requests returned 404, but their titles, languages, and recovery links differed.
It also limits the conclusion: this was how local preview handled path formatting in those checks, not evidence that the production host would take the same branch first. I did not change the normal URL policy just to make the preview pages match, nor did I describe untested host settings as a completed fix.
What those checks established
For those checks, I first read the routes, layout, language script, deployment configuration, and sitemap. I then inspected the build output, sent actual requests to local preview, and opened unknown English and Japanese URLs to confirm their content after the script ran.
| Target | Recorded result |
|---|---|
/about/work and the compatibility entry point /work |
Both returned 200 with readable work content and a canonical pointing to the primary URL |
| Unknown URLs under all three language prefixes, without a trailing slash | All returned 404 without a redirect and used the shared Traditional Chinese error HTML |
| Unknown English and Japanese URLs after JavaScript ran | The h1, lang, and homepage link switched to the corresponding language while preserving the unknown path |
| The same unknown URLs with a trailing slash | All returned 404, but used Astro’s path-format error page |
A direct request to 404.html |
Returned 200; used only to inspect the error-page file, not as an unknown-URL test |
These results did not cover the production host, caching, all browsers, or assistive technologies. They also did not prove that search engines had taken any indexing action.
After deployment, the actual domain still needs requests to normal deep routes, compatibility entry points, and unknown URLs with different language prefixes and trailing slashes. Read back the status codes, redirects, body content, and recovery links. Generating the error-page file and having the host use it correctly are two results that need separate verification.
Sources
[1] https://docs.astro.build/en/basics/astro-pages [2] https://developers.google.com/crawling/docs/troubleshooting/http-status-codes [3] https://developers.google.com/search/docs/crawling-indexing/block-indexing