Reader Stacks

Removing the # From Angular URLs (HTML5 Routing)

One line in Angular's router config switches URLs from /#/products to /products — the part that actually takes real work is configuring the web server to make those clean URLs work on refresh.

Angular's router defaults to "PathLocationStrategy" in modern versions, which already produces clean URLs without a hash — but older setups, or apps explicitly configured for hash-based routing, show URLs like /#/products instead of /products. Removing the hash is a one-line Angular change, but it shifts real responsibility onto the web server that wasn't needed before.

1. The Angular-side change

// app.config.ts (standalone) or the relevant NgModule
import { provideRouter, withHashLocation } from '@angular/router';

// Hash-based (has the #):
providers: [provideRouter(routes, withHashLocation())]

// Clean URLs (no #) — simply omit withHashLocation():
providers: [provideRouter(routes)]

Removing withHashLocation() (or, in older module-based apps, removing { useHash: true } from RouterModule.forRoot()) switches the router to use the browser's real History API instead of the URL's hash fragment for tracking route state.

2. Why this breaks page refresh and direct links without a server change

With hash-based routing, everything after the # is never actually sent to the server at all — the browser handles it entirely client-side, so the server only ever sees a request for / regardless of which "route" is showing. With clean URLs, the browser genuinely requests /products from the server on a refresh or a direct link — and unless the server is specifically configured to handle that, it returns a real 404, because no literal /products file or route exists on the server; only Angular's client-side router understands that path.

3. The fix: rewrite every non-file request to index.html

# nginx
location / {
    try_files $uri $uri/ /index.html;
}
# Apache — .htaccess in the deployed build's root
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

This tells the web server: if the requested path doesn't correspond to a real file (a JS bundle, a CSS file, an image), serve index.html instead of returning a 404 — Angular's router then boots up client-side and reads the actual URL path itself to render the correct route. Without this server-side rewrite configured, clean URLs work fine for in-app navigation but break on any page refresh or bookmarked/shared direct link.

4. This is required infrastructure, not optional polish

A common mistake is removing the hash in Angular, testing only by clicking through the app (which works fine, since it's all client-side navigation), and not testing a hard refresh or a direct URL visit before deploying — that's exactly the scenario that reveals the missing server rewrite rule, and it's easy to miss if refresh isn't specifically tested.

5. Setting the base href correctly

<!-- index.html -->
<base href="https://readerstacks.com/">

The <base href> tag needs to match wherever the app is actually deployed from — / for an app served from the domain root, or a subpath (like /app/) if it's deployed under one. A mismatched base href causes broken relative links and asset loading, a separate but related issue from the server rewrite rule above.

6. When hash-based routing is still the pragmatic choice

On hosting with no ability to configure server-side rewrites (some very basic static hosts, certain restrictive shared hosting setups), hash-based routing remains a reasonable, zero-configuration fallback — it trades cleaner-looking URLs for something that works reliably everywhere without any server configuration at all.

Topics: Deployment & Hosting