History API — SPA Routing with PHP Backend
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate semgrep can detect the pattern (pushState without Nginx try_files fallback) but automated detection is marked 'no', meaning it requires manual rule configuration and careful review. The bug is silent in development (local server often handles all routes) but breaks in production only when users refresh or share URLs — very close to d9 but the semgrep pattern at least provides a specialist path.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix mentions configuring Nginx try_files AND ensuring PHP handles all routes — this spans the frontend JS routing code, the Nginx/server config, and potentially PHP route handling. It's not a single-line patch but also not a full architectural rework; it crosses the client-server boundary touching at least two separate layers.
Closest to 'persistent productivity tax' (b5). The applies_to scope is web context only, but once a SPA routing strategy is committed to, every new route added must be handled both client-side (pushState/popstate) and server-side (Nginx/PHP fallback). This imposes an ongoing dual-maintenance cost on all routing work, slowing down feature development moderately but not reshaping the entire system architecture.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly states the canonical wrong belief: 'history.pushState handles server-side routing.' A developer familiar with server-side routing assumes that changing the URL means the server is aware of it, but pushState only changes the browser URL client-side. Refreshes and direct URL access bypass JS entirely, causing 404s — this directly contradicts how traditional URL routing works and catches many competent developers.
Also Known As
TL;DR
Explanation
history.pushState(state, '', url) changes the URL without a server request. popstate fires on back/forward navigation. PHP must handle all routes server-side (or return the SPA shell for any path) since a direct URL visit or refresh will hit PHP. Pattern: PHP routes all requests to index.php (via Nginx try_files), JavaScript handles client-side routing. replaceState for redirect-like URL updates without adding to history.
Common Misconception
Why It Matters
Common Mistakes
- Not configuring PHP/Nginx to serve SPA shell for all routes
- Not storing scroll position or page state in pushState's state object
- Forgetting popstate listener for back-button support
Code Examples
// pushState without server-side fallback:
// User types /dashboard directly → PHP 404 because no route
history.pushState({}, '', '/dashboard');
// Nginx: try_files $uri $uri/ /index.php
// PHP index.php: always renders the SPA shell
// JavaScript router:
function navigate(url) {
history.pushState({ scrollY: window.scrollY }, '', url);
renderPage(url);
}
window.addEventListener('popstate', (e) => {
renderPage(location.pathname);
if (e.state?.scrollY) window.scrollTo(0, e.state.scrollY);
});