Firebase Hosting rewrites — when to use them, when not to

Rewrites in Firebase Hosting let you map URLs to functions, Cloud Run, or single-page apps. Here is when each pattern is the right tool — and when rewrites cause more problems than they solve.

Rewrites are the most misused feature in Firebase Hosting. The right rewrite makes your SPA work and dynamic routes hit a Cloud Function. The wrong one silently flattens your site’s SEO or returns blank pages. This article walks through the four real use cases and the trap each one hides.

Background

A rewrite in firebase.json tells the CDN: “when the user requests path X, serve content Y.” Y can be a different static file, a Cloud Function, a Cloud Run service, or another Hosting site. Rewrites run before the 404 fallback, so they cover every URL that does not match a real file. That power is why people copy-paste rewrite blocks they do not understand.

How to tell

  • You have a SPA and want /anything to load /index.html.
  • You want /api/* to hit a Cloud Function or Cloud Run service.
  • You want to alias one path to another (e.g. /blog/* to /articles/*).
  • You want to serve content from another Firebase Hosting site at a path.

Quick verdict

Use rewrites only for the four cases above. For redirects (URL changes), use the redirects block instead. For most multi-page static sites, you do not need any rewrites at all.

Step by step

  1. Decide which case you actually have: SPA, dynamic backend, alias, or multi-site. Only one of these — mixing is where bugs start.

  2. SPA pattern. Use only on apps with client-side routing (React Router, Vue Router). Never on a multi-page static site:

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "**", "destination": "/index.html" }
    ]
  }
}
  1. Dynamic backend pattern. Scope to a prefix; never wildcard ** to a function. Example with both a Cloud Function and a Cloud Run service:
{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "/api/v1/**", "function": "api", "region": "us-central1" },
      { "source": "/og/**",     "run":      { "serviceId": "og-image", "region": "us-west1" } }
    ]
  }
}

Match Cloud Function’s deployed region exactly — a region mismatch is the #1 cause of “function not found” after a successful deploy.

  1. Multi-site pattern. Serve another Hosting site under a path — useful for status pages or docs subsites:
{
  "hosting": {
    "site": "marketing",
    "public": "dist",
    "rewrites": [
      { "source": "/docs/**", "site": "docs" }
    ]
  }
}
  1. For renaming/moving URLs use redirects, not rewrites — search engines need the 301 to update their index:
{
  "hosting": {
    "redirects": [
      { "source": "/blog/:slug",     "destination": "/articles/:slug", "type": 301 },
      { "source": "/old/**",         "destination": "/new/:0",         "type": 301 }
    ]
  }
}
  1. Deploy and verify in incognito with curl:
firebase deploy --only hosting

# Static file still served directly
curl -sI https://yourdomain.com/about.html | head -1
# HTTP/2 200

# API path hits the function
curl -sI https://yourdomain.com/api/v1/health | head -1
# HTTP/2 200

# Old URL redirects with 301
curl -sI https://yourdomain.com/blog/foo | grep -E 'HTTP|location'
# HTTP/2 301
# location: https://yourdomain.com/articles/foo
  1. Watch logs for the first 24 hours. If crawlers hit your rewrite for static assets (*.png, *.css), your rule is too broad:
firebase functions:log --only api --lines 200 | grep -E '\.(png|css|js|ico)' | head
# any output = your wildcard is matching too much

Common pitfalls

  • Wildcard rewrite to /index.html on a multi-page site — every URL becomes the same HTML, killing SEO.
  • Sending all traffic through a Cloud Function — slow, expensive, and unnecessary for static assets.
  • Using a rewrite when a redirect was the right tool. Rewrite keeps the URL; redirect changes it.
  • Forgetting that rewrites do not change the URL bar — search engines see the original path.
  • Missing cleanUrls interaction — if cleanUrls: true, /about.html is served at /about, and your rewrite may not match.

Who this is for

SPAs with client-side routing, sites with a dynamic API on Cloud Functions / Run, and multi-site setups that share one domain.

When to skip this

Plain multi-page static sites, sites that need true URL changes (use redirects), and apps where most traffic should be SSR — pick a framework-native host.

FAQ

  • What is the difference between rewrite and redirect?: Rewrite keeps the URL the user sees but serves different content. Redirect sends a 301/302 and the URL changes.
  • Can I rewrite to a Cloud Function and pass query params?: Yes. Query strings are forwarded automatically. Path segments matched by ** are available via the request URL.
  • Does the order of rewrites matter?: Yes. Firebase matches the first rewrite whose source matches. Put specific rules before catch-alls. When a rewrite seemingly does nothing in production, the cause is almost always order or a static file shadowing the source — walk through the diagnosis in rewrites not firing.
  • The rewrite deployed but the function returns 404 / “function not found”.: That is a function-region / name mismatch, not a rewrite bug — see Firebase function not found.
  • Why does my SPA rewrite break my sitemap?: Because ** matches /sitemap.xml too. Exclude it: scope your wildcard or put a more specific rule for /sitemap* first.

Tags: #Indie dev #Firebase #Hosting #Troubleshooting