← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

History API — SPA Routing with PHP Backend

JavaScript HTML5 Intermediate
debt(d7/e5/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

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.

e5 Effort Remediation debt — work required to fix once spotted

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.

b5 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

pushState popstate History API client-side routing

TL;DR

history.pushState and popstate enable URL changes without page reload — used for SPA routing while keeping PHP as the backend.

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

history.pushState handles server-side routing — pushState only changes the browser URL; if the user refreshes or shares the URL, PHP must handle that route or return a 404.

Why It Matters

PHP applications adding SPA-like navigation need both client-side routing (History API) and server-side fallback (Nginx try_files) to handle direct URL access.

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

✗ Vulnerable
// pushState without server-side fallback:
// User types /dashboard directly → PHP 404 because no route
history.pushState({}, '', '/dashboard');
✓ Fixed
// 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);
});

Added 17 Mar 2026
Edited 22 Mar 2026
Views 114
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 1 ping F 0 pings S 0 pings S 1 ping M 0 pings T 1 ping W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 1 ping T 1 ping W 0 pings T 1 ping F 0 pings S 0 pings S 1 ping M 0 pings T 1 ping W 0 pings T 1 ping F 1 ping S 2 pings S 1 ping M
Amazonbot 1
PetalBot 1 Twitter/X 1
Amazonbot 11 Google 9 Ahrefs 8 Perplexity 6 SEMrush 6 PetalBot 6 Unknown AI 5 Scrapy 4 ChatGPT 3 Twitter/X 3 Bing 3 Applebot 2 Majestic 1 Meta AI 1 Brave Search 1
crawler 66 crawler_json 1 your_contextpost 1 pre-tracking 1
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
JavaScript javascript The programming language of the browser — it reads and modifies the page (the DOM), reacts to user events, and fetches data without reloading.

JavaScript is the only language browsers execute, so every interactive behaviour on the web goes through it. Its two defining traits — single-threaded event loop and loose typing (== coercion) — explain the majority of both its bugs and its design patterns.

💡 Default to const, use === always, and reach for let only when a value genuinely reassigns.

Ask Codex about JavaScript →
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Configure Nginx try_files to fall back to PHP for all routes — pushState only works client-side; refreshes must be handled server-side
📦 Applies To
javascript HTML5 web
🔗 Prerequisites
🔍 Detection Hints
pushState without corresponding Nginx try_files fallback; no popstate listener for back navigation
Auto-detectable: ✗ No semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: File Tests: Update


✓ schema.org compliant