TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Why URL Fragments Don’t Reach Your Server

Practical insights, clear explanations, and useful ideas for understanding the web.

October 28, 20266 min read6 topics
Why URL Fragments Don’t Reach Your Server
Explore the details
#URL#SPA#JavaScript#Web Development#Routing#HTTP
October 28, 20266 min read

A URL like https://example.com/products?sort=price#reviews contains three parts that matter to a single-page application: a path, a query string, and a fragment. The browser sends the path and query string in an HTTP request, but it does not send the fragment. That distinction explains why hash-based routes work without server configuration—and why History API routes need server support when users open or refresh a deep link.

Key Takeaways

  • The fragment begins at #. Browsers do not include it in the HTTP request target.
  • The path and query string are sent to the server; the fragment is available to client-side JavaScript.
  • Hash-based routing stores the route in the fragment. History API routing stores it in the path, so direct visits and refreshes need a server fallback to the application.
  • Server logs cannot identify different fragment routes from the request alone. Client-side analytics can track them explicitly.

What the browser sends

Consider this URL:

https://example.com/products?sort=price#reviews

Its parts are:

PartExampleSent in the HTTP request?
Path/productsYes
Query string?sort=priceYes
Fragment#reviewsNo

For an HTTP/1.1 request, the request line would look like this:

GET /products?sort=price HTTP/1.1
Host: example.com

The fragment is for the browser. It can identify a section in a document or carry information that client-side code uses, but it is not part of the HTTP request target. The same distinction applies when the request is carried over newer HTTP versions: the URL fragment is not sent as part of the request target.

A fragment change does not, by itself, ask the server for a different resource. For example, clicking a link from https://example.com/docs#setup to https://example.com/docs#examples typically moves within the loaded document and updates browser history without fetching a new document. A navigation to a different page can trigger a request, but that request still omits the fragment.

Hash-based routing

Hash routers put the client-side route after #. The server receives the page path, not the route stored in the fragment.

For example, a user opens:

https://example.com/#/products/42

The browser requests / from the server. After the page loads, the application reads #/products/42 and renders the product view. A simplified example:

function renderRoute() {
  const route = window.location.hash.slice(1) || "/";
  console.log("Render route:", route);
}

window.addEventListener("hashchange", renderRoute);
renderRoute();

The hashchange event handles later fragment changes; the final call handles the route already present when the script starts. A router library would normally handle matching routes, rendering, and edge cases.

Useful when: you cannot configure the server to return your app for arbitrary paths, or you need a quick client-side routing scheme on static hosting with limited rewrite support.

Trade-offs: the # is part of the visible route, and the server cannot use the fragment to choose a response. Query-looking text inside the fragment—such as #/products/42?tab=reviews—is still fragment data, not the HTTP query string.

History API routing

History API routers use ordinary paths, such as /products/42?tab=reviews. Client-side navigation can update the address bar without requesting a new document:

function navigate(url) {
  history.pushState({}, "", url);
  renderRoute();
}

window.addEventListener("popstate", renderRoute);

// Called by the application's link handler:
navigate("/products/42?tab=reviews");

This is a simplified illustration, not a complete link-interception or routing implementation. The application must render the new route after calling pushState; that call does not automatically emit a popstate event. The popstate handler covers history traversal, such as pressing Back or Forward.

The important server-side difference appears on a direct visit or refresh. If someone opens https://example.com/products/42?tab=reviews, the browser requests /products/42?tab=reviews. Your server must respond appropriately—commonly by serving the SPA's entry HTML for application routes—before the client can render that route. Otherwise, a route that works after in-app navigation may return a server-side 404 when opened directly.

Configure the fallback narrowly. Application routes should reach the SPA entry point, while API endpoints and static assets should retain their intended responses. The client router also needs to handle unknown routes rather than rendering a valid page for every path.

Routing styleRoute locationWhat the server receives on a direct visitNeeds route fallback?
Hash-basedFragment, such as #/products/42The path and query before #Usually not for client routes
History APIPath, such as /products/42/products/42Yes, if the server otherwise does not serve the app there

Deep links, analytics, and navigation

Deep links and server configuration

With hash routing, different client-side routes can load the same server resource because the route is not in the request. With History API routing, the route is in the requested path, so the server and deployment platform need a suitable fallback for application routes. Test a route by opening it in a fresh tab and by refreshing it—not only by navigating to it from the app.

Analytics

Server access logs can record the requested path and query string, but not the browser's fragment. If you need analytics for hash routes, collect the route in the client and send an analytics event deliberately. Listen for hashchange when the fragment changes; for History API navigation, your router can report its own navigation events. Browser Back and Forward traversal can be observed with popstate.

Changing the address bar with pushState does not itself make an HTTP request. If client-side code sends an analytics request, that is a separate request created by the code; do not mistake it for the original page request containing a fragment.

Client-side navigation

The fragment can be useful for in-document navigation and client-side route state, but it is not a way to pass route information to a server. Put information in the path or query string when the server needs it. Avoid placing secrets in fragments: although fragments are not sent in the HTTP request, they remain visible to client-side code and can appear in copied URLs or browser history.

Common Mistakes

  • Expecting the server to see #section. It cannot read the fragment from the HTTP request. Client-side code can read window.location.hash.
  • Treating ? inside a fragment as the request query string. In /#/products?tab=reviews, everything after # belongs to the fragment.
  • Testing only in-app navigation. A History API route may work after pushState but fail on refresh or a direct visit if the server lacks a fallback.
  • Assuming pushState triggers rendering or popstate. Call your renderer after pushing state; use popstate for history traversal.
  • Expecting server analytics to distinguish hash routes. Track fragment-based views in client-side analytics if needed.

Best Practices

  • Choose hash routing when server rewrites are unavailable and its URL format fits your needs.
  • Choose History API routing when clean paths are important, and configure and test server fallbacks for direct visits and refreshes.
  • Keep API routes and static asset handling separate from any SPA fallback.
  • Treat the fragment as client-side URL state, not as data sent to the server.
  • Test initial load, in-app navigation, refresh, Back and Forward, unknown routes, and analytics separately.

Conclusion

URL fragments are browser-side data: the browser leaves them out of HTTP requests. Hash routers use that behavior to keep routes out of server requests, while History API routers put routes in paths and therefore require the server to support deep links. Choose based on your hosting constraints and verify both client navigation and direct page loads.

References