SPA vs serverrouting – sådan undgår du 404 på Netlify, Vercel og GitHub Pages
Har du nogensinde refreshet en underside i din SPA og blevet mødt af en 404?
Du deployer din React-, Vue- eller Svelte-app. Alt spiller på forsiden. Men så går du til /dashboard, trykker refresh… og får en fin, kold 404 fra serveren.
Lokalt virkede alt. Selvfølgelig. For der kører en dev-server, som er bygget til SPA routing. Din host gør noget andet: den prøver at finde en fysisk fil på den sti, du har skrevet.
For at fikse det er du nødt til at skelne mellem to ting: serverrouting og clientrouting.
Hvorfor din SPA får 404 efter deploy
En klassisk SPA (Single Page Application) bruger typisk History API routing. Det betyder, at alle dine sider i virkeligheden er JavaScript, der kører i browseren, efter index.html er loadet.
Forestil dig, at du har en React Router-konfiguration som:
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
Din app kender /about og /dashboard. Men din host gør ikke. Når du skriver /dashboard i adresselinjen, sker der to ting:
1) Browseren sender en HTTP-request til serveren på stien /dashboard.
2) Serveren leder efter en fil/mappe ved navn dashboard.
Hvis du ikke har en fysisk /dashboard/index.html eller lignende, svarer serveren med 404. Den ved ikke, at din app ville have håndteret den route, hvis bare den havde fået lov at loade index.html først.
Det, du vil, er egentlig: “Giv altid index.html tilbage, og lad mit JS håndtere resten”, med mindre der rent faktisk findes en fil på den sti.
Det mønster kaldes ofte “History API fallback”. Og det er præcis det, du skal konfigurere på Netlify, Vercel og GitHub Pages.
Er det faktisk en SPA-fejl? Hurtig diagnose
Inden du begynder at rode i configs, er det rart lige at tjekke, om problemet faktisk handler om SPA-routing og ikke bare en slå-fejl i et filnavn.
Mini-tjek: opfører din app sig som en SPA?
Et par hurtige indikatorer:
Hvis du klikker rundt mellem “siderne” i navigationen, og siden ikke reloades fuldt (ingen hvid skærm, ingen Favicon-blink), så kører du clientside routing.
Hvis dine links ser sådan ud i React:
<Link to="/about">About</Link>
eller i Vue Router:
<router-link to="/about">About</router-link>
og ikke som:
<a href="/about">About</a>
så er det også et tegn på SPA-routing.
Typisk symptom: kun 404 når du refresher eller går direkte til en underside
Det klassiske billede:
Du kan:
– Åbne forsiden
– Klikke til /about og /dashboard via links i appen
Men du får 404, hvis du:
– Refresher på /about
– Indsætter URL’en til /dashboard direkte i browseren
Hvis det er sådan, er du landet det rigtige sted. Nu handler det om at lære din host at sende alt tilbage til index.html.
Netlify vs SPA-routing – brug _redirects eller netlify.toml
Netlify er faktisk ret venlig over for SPA’er. Du skal bare fortælle den, at alle ukendte paths skal pege på din index.html.
Metode 1: _redirects-fil
Den mest lige-til løsning er en fil i din build-output-mappe (typisk dist eller build) med navnet _redirects.
Indholdet kan være så simpelt som:
/* /index.html 200
Det betyder: For alle paths (/*), giv /index.html tilbage med statuskode 200.
Vigtige detaljer:
– Filen skal ende i din public/build-mappe, som Netlify faktisk deployer.
– Ingen ekstra quotes, ingen kolon, bare linjen som ovenfor.
– Pas på editoren ikke kalder den _redirects.txt.
En typisk React-opsætning:
my-app/
public/
index.html
_redirects
src/
package.json
Med npm run build bliver _redirects kopieret til build/, og så læser Netlify den.
Metode 2: netlify.toml
Hvis du hellere vil holde ting i en config-fil, kan du bruge netlify.toml i roden af dit repo:
[build]
command = "npm run build"
publish = "build"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Pointen er stadig den samme: alt udefineret falder tilbage til index.html.
Hvis du vil ned ad kaninhullet med flere varianter, har Netlify selv ret fine eksempler i deres docs: Netlify redirects.
Vercel vs SPA-routing – brug rewrites i vercel.json
På Vercel skal du tænke i “rewrites” i stedet for redirects. En rewrite omskriver stien internt, uden at ændre URL’en i browseren.
vercel.json med rewrite til index.html
Læg en vercel.json i roden af projektet:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Det betyder: Uanset hvad brugeren beder om, så serv /index.html. Din SPA-router tager over derfra.
Hvis du har API-routes eller statiske filer, du ikke vil overskrive, kan du være mere specifik, fx:
{
"rewrites": [
{
"source": "/(about|dashboard|settings)",
"destination": "/index.html"
}
]
}
eller bruge en kombination med routes/redirects, alt efter hvordan din app er skruet sammen. Se Vercels egne eksempler på SPA’er i Vercel rewrites-dokumentationen.
Byg-output: sørg for at det er en static build
Hvis du bruger f.eks. Next.js i pure SPA-mode, er der lidt andre mønstre, men hvis du står med en klassisk create-react-app eller Vite SPA, handler det typisk om to ting:
– At npm run build laver en statisk mappe (ofte dist eller build).
– At Vercel peger på den som output.
Det klarer Vercel som regel selv via framework-detection, men det er værd at dobbelttjekke i dashboardet, hvis tingene opfører sig mystisk.
GitHub Pages vs SPA-routing – brug 404.html-tricket
GitHub Pages er lidt mere stædig. Du har ikke samme fleksible routing som på Netlify og Vercel. Men der er et velkendt hack: 404.html-tricket.
Idéen: lad 404.html indlæse din SPA og omdirigere
GitHub Pages viser automatisk en 404.html, hvis den ikke finder en given sti. Det kan du udnytte ved at lave en 404-side, der i praksis er din SPA eller et lille script, der loader den.
Den nemme version er at kopiere index.html til 404.html i din build-output-mappe. Når en bruger går til /about, finder GitHub Pages ikke en fysisk fil, viser 404.html, som så er din app, og din router matcher /about.
En forenklet proces kunne være:
npm run build
cp build/index.html build/404.html
Det kan du lægge ind i et lille deploy-script.
React Router + basename når du hoster i et subpath
Hvis din GitHub Pages-side ligger på https://brugernavn.github.io/repo-navn/, så ligger din SPA ikke på roden (/), men på /repo-navn/. Det skal din router kende.
I React Router kan du bruge basename:
import { BrowserRouter } from "react-router-dom";
function App() {
return (
<BrowserRouter basename="/repo-navn">
{/* dine routes her */}
</BrowserRouter>
);
}
Og i din package.json kan du sætte:
{
"homepage": "https://brugernavn.github.io/repo-navn/"
}
Så bygger create-react-app med de rigtige asset-paths. Det samme princip gælder, hvis du bruger andre bundlere: du skal sikre, at publicPath/base-URL peger det rigtige sted hen.
Hvis du gerne vil forstå forskellen på roddomæne og undermapper lidt mere, har vi også artikler om URL-struktur i forbindelse med f.eks. HTML-mappestruktur, som minder meget om samme problem.
Typiske faldgruber med SPA-routing efter deploy
Selv når dine redirects/rewrites er sat op, kan der være småting, der driller. Her er de hyppigste, jeg støder på.
Assets loader ikke, når du går direkte til en underside
Du kan f.eks. se, at CSS eller billeder forsvinder, hvis du åbner /about direkte. Ofte handler det om relative stier som:
<link rel="stylesheet" href="styles.css" />
<img src="images/logo.png" />
Når du står på /about, leder browseren efter /about/styles.css. Det findes ikke.
Brug i stedet stier med rod:
<link rel="stylesheet" href="/styles.css" />
<img src="/images/logo.png" />
eller (i bundleren) sørg for, at du importerer assets via JS/CSS, så den håndterer paths for dig.
Mix af serverroutes og SPA-routes
Hvis du både har rigtige serverroutes (f.eks. et API) og SPA-routes, skal dine regler være lidt mere præcise.
Eksempel på Netlify med API under /.netlify/functions og SPA for resten:
/.netlify/functions/* /.netlify/functions/:splat 200
/* /index.html 200
Samme idé i Vercel med rewrites og functions-mapper.
Hvis du går i gang med backend-delen også, giver det faktisk mening at forstå det hele som to lag: serverrouting til API og statiske filer, og clientrouting til “side-skift”. Det er det, jeg typisk bygger op fra bunden i mere full stack-orienterede projekter, som dem vi også beskriver i artikler om f.eks. intro til full stack.
Tjekliste – sådan tester du din SPA-routing efter deploy
Når du føler, du er færdig, er her en hurtig manuelt-test, som fanger 90 % af fejlene.
1. Start på forsiden
Åbn dit domæne uden ekstra path. Tjek:
– Loader appen uden fejl i konsollen?
– Er der 200-svar på HTML, JS og CSS i Network-tabben?
2. Klik rundt internt
Brug dine links til /about, /dashboard osv. Kig efter:
– Skifter URL’en i adresselinjen?
– Er der undgået full page reloads (SPA-beskyldningen)?
3. Refresh på en underside
Stå på f.eks. /about og tryk F5 eller Cmd+R. Du vil se en af to ting:
– 200 på HTML-filen (godt, History API fallback virker).
– 404 på HTML-filen (din redirect/rewrite er ikke ramt).
4. Åbn en underside i ny fane
Højreklik på linket til f.eks. /dashboard og vælg “Åbn link i ny fane”. Eller indsæt URL’en direkte. Appen skal stadig loade og vise den rigtige view.
5. Tjek for konsolfejl om assets
Hvis noget ikke ser rigtigt ud, kigger jeg altid i browserens console og Network-tab først. 404 på JS/CSS filer afslører som regel path-problemer, ikke routing-config.
Hvis du vil blive stærkere til at debugge i browseren generelt, er det faktisk en kæmpe hjælp at lære de basale værktøjer i f.eks. Chrome DevTools, som vi blandt andet bruger igen og igen i små øvelser i artikler som denne om JavaScript debugging.
Til sidst – det er ikke magi, det er bare to forskellige hjerner
Hele problemet her handler om, at din server tænker i filer og mapper, mens din SPA tænker i routes og komponenter. Så snart du får serveren til konsekvent at give index.html tilbage og lader JavaScript tage over, forsvinder 404’erne typisk af sig selv.
Og hvis du lige har siddet en aften og kæmpet med det, fordi alt virkede “jo fint lokalt” – velkommen i klubben. Jeg har personligt brugt længere tid på en manglende _redirects-fil, end jeg nogensinde vil indrømme over for andre end en logfil.









1 kommentar