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

What Happens to the URL Fragment in a Single-Page App?

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

October 28, 20268 min read5 topics
What Happens to the URL Fragment in a Single-Page App?
Explore the details
#URL#single-page applications#client-side routing#web development#accessibility
October 28, 20268 min read

A URL such as https://example.com/products#details contains a fragment after the #. Browsers use fragments for in-page navigation, and single-page apps can use them to represent client-side routes. But the fragment is not included in the HTTP request sent to the server. That distinction affects routing, deep links, refreshes, and how you make navigation accessible.

Key Takeaways

  • The browser does not send a URL fragment to the server in an HTTP request. For https://example.com/products#details, the request concerns /products, not #details.
  • A hash-based router uses the fragment as client-side state, so the server normally receives only the app's base document URL.
  • A path-based router uses paths such as /products; the server must be configured to serve the app for valid client-side routes when users load or refresh them directly.
  • For in-page navigation, use real links and meaningful target elements. For app routes, make sure direct links and browser history work as expected.

What a URL fragment is—and what the server receives

In https://example.com/products?sort=price#details, the fragment is details, introduced by #. The URL's scheme, authority, path, and query can be relevant to a request; the fragment identifies a location or state for the user agent to handle. It is not part of the HTTP request target sent to the server.

For example, navigating to:

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

sends a request for the resource identified by the path and query, equivalent in this respect to:

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

The browser then handles #details on the client side. It may scroll to an element whose id is details, or application code may interpret the fragment another way. The server cannot use the fragment to decide which response to return because it never receives it as part of that request.

The fragment is not secret or a secure place for data: it remains visible in the address bar and is available to client-side code. Also, other mechanisms—such as analytics code running in the browser—may observe it. Do not put credentials or sensitive information in a fragment.

How hash-based and path-based SPA routing differ

A single-page app can update the displayed view without asking the server for a new HTML document on every navigation. Routers commonly encode the current route in one of two places.

Routing styleExample URLWhat the server receives on direct load or refreshTypical requirement
Hash-basedhttps://example.com/#/productsA request for /The app reads the fragment and renders the route.
Path-basedhttps://example.com/productsA request for /productsThe server must serve the app entry point for client-side routes, while still serving real assets and handling genuine missing resources correctly.

Hash-based routing

With a hash router, the part after # is application state. A minimal example can read the route on startup and when the fragment changes:

const app = document.querySelector("#app");

function renderRoute() {
  const route = window.location.hash.slice(1) || "/";

  const pages = {
    "/": "Home",
    "/products": "Products",
    "/about": "About"
  };

  app.textContent = pages[route] ?? "Page not found";
}

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

This example assumes the document contains an element such as <main id="app"></main>. Links can use href="#/products" and href="#/about". The initial call matters: a page loaded directly at /#/products needs to render the route even though no hashchange event occurred during that page load.

A hash router is useful when you cannot configure server-side route fallback, such as on some static hosting setups. Its trade-off is that the fragment now represents app navigation rather than ordinary in-page targets. A router that uses #/products must account for that if the same page also needs links to sections.

Path-based routing

A path router can use the History API to change the URL without a full document navigation. For example, history.pushState() can add a route to the address bar, while popstate lets the app respond when a user goes back or forward:

function renderPath() {
  const route = window.location.pathname;
  const pages = {
    "/": "Home",
    "/products": "Products",
    "/about": "About"
  };

  document.querySelector("#app").textContent = pages[route] ?? "Page not found";
}

function navigate(path) {
  window.history.pushState({}, "", path);
  renderPath();
}

window.addEventListener("popstate", renderPath);
renderPath();

This is a simplified illustration, not a complete router: production navigation also needs to handle links, modifiers such as Ctrl or Command-click, route parameters, and not-found pages. Crucially, pushState() does not make the server serve a route on a later direct load. If a visitor opens /products in a new tab or refreshes it, the browser requests /products from the server. Configure the server or hosting platform to return the SPA entry point for app routes, without rewriting API requests or static asset requests incorrectly.

Deep links, refreshes, and deployment checks

A deep link is a URL that opens a particular page or state directly, rather than requiring the visitor to start at the home page and navigate there. Both routing styles can support deep links, but the server behavior differs:

  1. Hash route: Open https://example.com/#/products. The server receives a request for /; after the app loads, its router reads #/products and renders the products view.
  2. Path route: Open https://example.com/products. The server receives a request for /products; it must return the app entry point for that route if the route belongs to the SPA.
  3. Refresh: A hash route still requests the same base path. A path route requests its full path again, so a missing fallback commonly causes a server 404 even though client-side navigation appeared to work.
  4. Back and forward: Test these controls, not just clicks within the app. Hash changes produce hashchange; History API traversal produces popstate. The app should update its view to match the URL.

A useful deployment test is to paste a nested route into a fresh browser tab, then refresh it. Also test an unknown route and a missing asset: a broad fallback should not make every missing file appear to load successfully as the app shell.

In-page fragments and accessibility

For navigation to a section of the current document, use a normal link and a matching, stable id:

<a href="#shipping">Shipping information</a>

<section id="shipping" aria-labelledby="shipping-heading">
  <h2 id="shipping-heading">Shipping information</h2>
  <p>Details about shipping options and delivery.</p>
</section>

This gives users a meaningful link destination, supports keyboard use, and lets browsers provide their native fragment-navigation behavior. Use descriptive link text rather than labels such as “click here.” Ensure the target is not hidden behind a fixed header; CSS such as scroll-margin-top can help account for that layout.

When client-side navigation replaces or substantially changes a view, consider focus and context as well as the address bar. Users who navigate with a keyboard or assistive technology need to understand that the view changed. Depending on the design, move focus to the new page's main heading or announce the change appropriately, and avoid unexpected focus jumps for ordinary in-page links. Preserve semantic links and test the resulting behavior with keyboard navigation and assistive technology.

Common Mistakes

  • Expecting the server to receive the fragment. It does not. If server logic needs a value, put it in a path, query parameter, request body, or another appropriate mechanism.
  • Assuming client-side navigation proves refreshes work. A route can render after an in-app click yet fail when opened directly because the server does not provide the SPA entry point.
  • Treating every # as a router route. Fragments also support in-page navigation. Decide how the app distinguishes route state from section links.
  • Listening only for hashchange in a path router. History API navigation uses different events; handle initial rendering and back/forward behavior too.
  • Using fragments for secrets. A fragment is exposed in the browser and to client-side code; it is not an access-control or confidentiality mechanism.
  • Replacing accessible links with click-only controls. Keep navigation discoverable and operable with a keyboard, and give destinations meaningful names.

Best Practices

  • Choose hash routing when its deployment simplicity is useful and fragment-based URLs fit the product. Choose path routing when clean resource-like URLs are important and you can configure the server fallback.
  • Test initial load, direct deep links, refresh, back/forward, unknown routes, and missing assets in the deployed environment.
  • Keep server fallback rules scoped to application routes; do not accidentally mask API errors or missing static files.
  • Use native fragment links for document sections. Use a router for app views, and manage focus or announcements when those views change.
  • Treat anything in a URL as user-visible input. Validate and encode values according to their use; never rely on a URL component as authorization.

To inspect the components of a URL while debugging, use a URL parser, but remember that parsing a URL does not change what the browser sends to the server or configure routing.

Conclusion

The # fragment is handled by the browser, not included in the HTTP request. Hash routers take advantage of that by storing client-side routes after #; path routers put routes in the requested path and therefore need appropriate server handling for direct loads and refreshes. Test both the URL behavior and the user experience, including keyboard and assistive-technology navigation.

References