Panduan SPA SEO
cara membuat single-halaman applications (React Router, Vue Router, Angular Router) dapat di-crawl dan dapat diindeks — app-shell dan soft-404 masalah, History API vs hash routing, per-route HTML via SSR/prerendering, per-route canonicals dan judul, dan sitemap generation untuk client-side routes.
Bahasa
sebuah single-halaman application memuat one document dan swaps views dengan JavaScript alih-alih requesting sebuah baru halaman dari server. banyak SPAs implement itu dengan sebuah 'app shell' — server mengembalikan one URL's worth dari nyata HTML, sebuah near-empty shell, until JS renders setiap route — tetapi itu's sebuah umum implementation choice, not sebuah aturan setiap SPA mengikuti; periksa what sebuah direct permintaan untuk setiap route actually mengembalikan sebelum assuming ini. fix, where sebuah bare app-shell setup adalah masalah, memiliki two independent halves: addressability (History API, not hash/#! fragments, so setiap view memiliki sebuah nyata URL — sebuah browser soft navigation via History API perubahan URL dan UI tetapi doesn't itself buat baru server respons) dan konten availability (SSR, prerendering, atau sebuah meta-framework so setiap dari itu URLs dapat kembalikan unique HTML pada permintaan). pada top dari itu, verify setiap route's judul, canonical, dan robots state both pada direct entry dan setelah rendering, handle soft 404s (routers tend untuk pertahankan sebuah 200 status untuk 'tidak ditemukan' views — redirect untuk sebuah URL itu itself 404s, atau tambahkan sebuah rendered noindex, though sebuah initial noindex dapat cause rendering untuk menjadi skipped), dan pastikan setiap route di Anda sitemap actually resolves untuk nyata, dapat diindeks HTML — there's no special SPA sitemap format.
TL;DR — sebuah single-halaman application (SPA) memuat one halaman dari server dan lalu menggunakan JavaScript untuk switch “pages” (terjemahan) “halaman” without sebuah full reload. catch: banyak SPAs kirim yang sama near-empty starting halaman no penting which URL sebuah mesin pencari menanyakan untuk — itu’s sebuah umum risk dari how SPAs adalah biasanya dibangun, not sebuah guarantee dari setiap SPA. untuk fix ini, pastikan setiap route memiliki -nya own nyata URL dan -nya own nyata HTML — biasanya oleh rendering halaman pada server atau pre-membangun them.
What sebuah SPA adalah
sebuah single-halaman application commonly updates client-side views dan routes without sebuah full document navigation. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Google dapat render JavaScript SPAs, tetapi dapat di-crawl URLs, tautan, status handling, dan rendered konten remain necessary. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
sebuah single-halaman application adalah sebuah situs web dibangun sebagai one HTML halaman. When Anda click sekitar — dari products halaman untuk tentang halaman — JavaScript swaps out what’s pada screen alih-alih asking server untuk sebuah whole baru halaman. React (dengan React Router), Vue (dengan Vue Router), dan Angular semua berfungsi ini cara oleh default. ini feels fast dan app-like, which adalah why ini adalah popular.
risk adalah what server mengirim. banyak SPAs gunakan “app shell” (terjemahan) “app shell” implementation:
pertama time anyone (including Google) memuat app, server mengembalikan sebuah
mostly-empty shell, dan nyata konten gets dibangun di browser afterward. itu’s
one umum cara untuk bangun sebuah SPA — not definition dari one, dan not something setiap
SPA melakukan. tetapi when sebuah situs melakukan ship sebuah bare app shell, failure mode adalah nyata: jika
Google menanyakan server untuk /about dan /products separately, ini dapat get exact
sama blank shell untuk both, because nothing tentang route changed what server
dikirim. cara untuk know whether Anda app adalah di itu position adalah untuk periksa what sebuah direct
permintaan untuk setiap route actually mengembalikan — not untuk assume ini dari fact itu ini adalah sebuah
SPA.
Why itu hurts SEO
mesin pencari perlu untuk see Anda konten untuk peringkat ini. dengan sebuah bare SPA, three things tend untuk go wrong:
- setiap URL looks yang sama untuk server. Deep tautan, shares, dan crawler semua land pada yang sama shell.
- 404 halaman masih say “200 OK.” (terjemahan) “200 OK.” sebuah JavaScript router dapat tampilkan sebuah “not found” (terjemahan) “tidak ditemukan” screen while halaman technically reports success, so Google dapat indeks empty halaman.
- ** wrong URLs.** Older SPAs digunakan addresses like
example.com/#/products. Google dapat’t reliably indeks itu.
cara fix ini ( pendek versi)
- Give setiap view sebuah nyata URL menggunakan browser’s History API (clean paths like
/products), not#-based ones. - kirim nyata HTML untuk setiap URL. Render halaman pada server (SSR) atau pre-bangun them ahead dari time (prerendering). jika Anda’re starting fresh, sebuah framework itu melakukan ini untuk Anda — Next.js, Nuxt, SvelteKit, Angular’s SSR — saves Anda trouble.
- Give setiap halaman -nya own judul dan deskripsi itu perubahan when route perubahan.
- membuat tautan nyata tautan (
<a href>), not clickable<div>s.
ingin mechanics — why hash routing fails, “most restrictive directive” (terjemahan) “sebagian besar restrictive directive” trap dengan canonicals, dan cara generate sebuah sitemap untuk client-side routes? Switch untuk Advanced tab.
TL;DR — sebuah SPA’s core SEO risk isn’t “JavaScript” (terjemahan) “JavaScript” di abstract — ini adalah itu client-side routing perubahan what pengguna sees tanpa mengubah what server akan kembalikan. fix memiliki two independent halves people constantly conflate: addressability (History API, unique per-route URLs, no
#!fragments) dan konten availability (SSR, prerendering/SSG, atau sebuah meta-framework). melakukan hanya pertama dan Anda get sebuah tidy sitemap dari URLs itu semua render yang sama shell. pada top dari both, setiap route perlu -nya own canonical/judul/deskripsi di rendered DOM, Anda memiliki untuk handle soft 404s itu pertahankan sebuah200, dan setiap route di sitemap memiliki untuk independently resolve untuk nyata HTML.
What SPA SEO actually adalah
SPA adalah sebuah application architecture, not sebuah guaranteed SEO failure state. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application server rendering, prerendering, atau carefully implemented client rendering dapat expose konten, tetapi none guarantees pengindeksan. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
ini adalah deep dive pada single-halaman applications specifically — client-side routing dengan React Router, Vue Router, atau Angular Router, where setelah pertama muat server tidak pernah mengirim lainnya full halaman. ini sits alongside broader JavaScript SEO guide (which covers parity, lazy-memuat, infinite scroll, dan JS rendering umumnya) dan per-framework write-ups untuk React, Next.js, Nuxt, Angular, Vue, Svelte, dan Astro. Here I’m hanya interested di routing layer dan what ini melakukan untuk crawling dan pengindeksan.
Why bare client-side-routed SPAs fail di SEO
One URL, one HTML respons — app-shell masalah
Google describes failure mode precisely: “Some JavaScript sites may use the app
shell model where the initial HTML does not contain the actual content and Google needs
to execute JavaScript before being able to see the actual page content.” (terjemahan) “beberapa JavaScript situs dapat gunakan app
shell model where initial HTML melakukan not berisi actual konten dan Google perlu
untuk execute JavaScript sebelum menjadi able untuk see actual halaman konten.” di sebuah bare
SPA, “app shell” (terjemahan) “app shell” adalah semua server ever mengembalikan. Fetch /products fresh dan Anda
get yang sama near-empty document Anda’d get untuk /about. konten hanya diverges
setelah browser runs Anda JavaScript dan Anda router decides what untuk tampilkan.
itu’s whole masalah di one kalimat: client-side routing perubahan what pengguna sees tanpa mengubah what server akan kembalikan. Everything downstream — soft 404s, duplicate konten, missing judul — adalah sebuah symptom dari itu.
server tidak pernah sees which “page” (terjemahan) “halaman” adalah requested (why hash routing fails)
Older SPAs muat views dari URL fragments — example.com/#/products. Google’s exact
kata: “A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” (terjemahan) “sebuah SPA dapat gunakan URL fragments (misalnya undefined untuk
memuat berbeda views.” ini fails untuk sebuah spesifik, mechanical alasan: browser
tidak pernah kirim fragment (anything setelah #) untuk server di HTTP permintaan.
server literally cannot know which “page” (terjemahan) “halaman” adalah requested, so ini dapat’t kembalikan berbeda
konten atau sebuah berbeda kode status untuk ini. sebuah crawler itu fetches URL once gets
identical HTML regardless dari fragment.
itu’s juga why Google formally deprecated -nya 2009 AJAX-crawling scheme back di
October 2015: “In short: We are no longer recommending the AJAX crawling proposal we
made back in 2009.” (terjemahan) “di pendek: kami adalah no longer recommending AJAX crawling proposal kami
dibuat back di 2009.” old _escaped_fragment_ workaround let server pre-render
fragment routes pada permintaan — tetapi ini patched sekitar routing masalah alih-alih
fixing ini. Google’s own recommendation replacing ini adalah History API, covered below.
Soft 404s — client-side routers pertahankan sebuah 200 untuk everything
ini one adalah close untuk inevitable di sebuah bare SPA. Client-side routers, oleh design, pertahankan
original halaman’s 200 status untuk setiap virtual navigation — including “not found” (terjemahan) “tidak ditemukan”
states. Google flags ini explicitly: “In a single-page application (SPA), this can be
especially difficult. To prevent error pages from being indexed, you can use one or both
of the following strategies.” (terjemahan) “di sebuah single-halaman application (SPA), ini dapat menjadi
terutama difficult. untuk mencegah halaman error dari menjadi terindeks, Anda dapat gunakan one atau both
dari following strategies.” dan why: “When a SPA is using client-side JavaScript
to handle errors they often report a 200 HTTP status code instead of the appropriate
status code.” (terjemahan) “When sebuah SPA adalah menggunakan client-side JavaScript
untuk handle errors mereka sering report sebuah undefined HTTP kode status alih-alih appropriate
kode status.”
hasil adalah empty atau error views getting terindeks sebagai thin 200 halaman. Google
documents exactly two strategies here, precisely: redirect (atau membuat sebuah full permintaan) untuk
sebuah URL whose server mengembalikan sebuah nyata 404/error status, atau tambahkan sebuah noindex tag untuk
error view dengan JavaScript. Watch kedua one: jika itu noindex adalah present dari
very pertama paint alih-alih ditambahkan setelah app decides route adalah invalid, ini dapat
cause Google untuk skip rendering halaman altogether — so verify what sebuah direct permintaan
mengembalikan sebelum apa pun JS runs, not hanya what menampilkan up di rendered DOM afterward.
Shared-shell duplicates — sebuah mungkin cause, not sebuah automatic diagnosis
There’s sebuah kedua, sneakier failure mode: distinct routes get terindeks sebagai duplicates dari setiap lainnya because mereka render down untuk yang sama shared header/nav/footer boilerplate. Gary Illyes described one cara ini happens: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (terjemahan) “I memiliki sebuah bunch dari emails di my inbox where issue adalah itu centerpiece took forever untuk muat, so rendering timed out (my sebagian besar mungkin explanation) dan kami adalah left dengan sebuah bunch dari halaman itu hanya memiliki boilerplate. dengan hanya boilerplate, itu halaman adalah dups.” His fix: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (terjemahan) “Try untuk restructure js panggilan such itu konten (including marginal boilerplate) memuat pertama.” (Relayed via sebuah LinkedIn post, which resists automated verification; treat sebagai tinggi-confidence secondary.)
Notice Illyes frames ini sebagai “my most likely explanation,” (terjemahan) “my sebagian besar mungkin explanation,” not sebuah confirmed diagnosis — itu’s worth taking literally. “Render timeout” (terjemahan) “Render timeout” isn’t something Anda dapat diagnose dari symptoms alone; Anda perlu permintaan-tingkat evidence (what sebuah direct fetch actually mengembalikan), rendered-output evidence (whether centerpiece konten adalah present setelah rendering), dan Search Console evidence (duplicate/pengindeksan signals) sebelum attributing sebuah route’s duplicate-konten masalah untuk sebuah timeout specifically, alih-alih untuk sebuah bug, sebuah blocked resource, atau sebuah genuinely identical shell. There’s juga no fixed, published timeout untuk design sekitar — Google’s own basics doc says sebuah halaman “may stay on this queue for a few seconds, but it can take longer than that,” (terjemahan) “dapat stay pada ini queue untuk sebuah few seconds, tetapi ini dapat take longer daripada itu,” without committing untuk sebuah angka. Don’t plan sekitar sebuah assumed render-queue duration; instead, pastikan primary konten memuat sebagai early sebagai mungkin regardless dari how panjang rendering ends up taking.
fix: get nyata HTML per route
full fix memiliki two independent bagian itu people conflate constantly:
- URL addressability — History API, unique per-route URLs, no fragments.
- konten availability — SSR, static prerendering/SSG, atau sebuah meta-framework.
melakukan hanya #1 dan Anda get clean, shareable URLs itu semua masih mengembalikan sama blank shell. untuk sebuah route Anda actually ingin terindeks, Anda perlu both.
itu’s not sebuah universal mandate untuk tambahkan SSR everywhere, though. SSR dan prerendering reduce how much Anda depend pada sebuah crawler successfully executing Anda JavaScript — mereka don’t become mandatory instant sebuah situs adalah technically sebuah SPA. sebelum picking sebuah architecture untuk sebuah given route, periksa three things: whether itu route perlu untuk peringkat di semua (sebuah internal admin panel doesn’t), what sebuah direct, JS-free permintaan untuk ini sudah mengembalikan (beberapa setups sudah kirim meaningful HTML), dan which crawler Anda actually perlu untuk satisfy (Google renders JS fairly reliably; Bing dan sebagian besar AI crawler less so — see below). CSR itu sudah passes itu memeriksa doesn’t perlu untuk become SSR hanya because situs adalah sebuah SPA.
rendering sisi server (SSR)
server runs Anda app untuk setiap permintaan dan mengembalikan fully-formed HTML untuk itu route, lalu client “hydrates” (terjemahan) “hydrates” ini ke sebuah live SPA. ini adalah paling robust option because crawler gets complete konten pada pertama fetch, no JS execution diperlukan.
Static prerendering / SSG
alih-alih rendering per permintaan, Anda bangun setiap route’s HTML ahead dari time di deploy. Perfect untuk konten itu doesn’t perubahan per pengguna. dari my own JavaScript SEO writing: apa pun jenis dari SSR, static rendering, dan prerendering setup adalah going untuk menjadi fine untuk search mesin — thing untuk hindari adalah leaving konten locked behind client-hanya rendering.
atau: don’t hand-roll ini — gunakan meta-framework
untuk sebuah baru bangun, honest recommendation adalah untuk not hand-roll React-Router-hanya atau Vue-Router-hanya client-side routing di semua. sebuah meta-framework — Next.js, Nuxt, Angular dengan -nya SSR package, SvelteKit, Remix — gives Anda SSR/SSG dan route-based metadata out dari box, which sidesteps ini entire category dari masalah. (setiap dari itu memiliki -nya own deep dive pada ini situs.) menjadi honest tentang flip side: retrofitting SSR onto sebuah existing hand-rolled SPA adalah nyata engineering berfungsi — sebuah migration project, not sebuah config toggle.
Dynamic rendering sebagai sebuah stopgap, not sebuah destination
Anda dapat sajikan crawler sebuah separately-rendered HTML snapshot (dynamic rendering). Both Google dan Bing accept ini — Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (terjemahan) “recommend[s] dynamic rendering sebagai sebuah great alternative untuk situs web relying heavily pada JavaScript” (relayed dari Bing’s 2018 blog; two pendek bingbot-capability quotes below adalah directly verified, ini longer one adalah not independently re-diperiksa) — tetapi Google adalah jelas ini adalah sebuah workaround: “Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (terjemahan) “Dynamic rendering adalah sebuah workaround dan not sebuah panjang-istilah solusi… Instead, kami recommend itu Anda gunakan rendering sisi server, static rendering, atau hydration sebagai sebuah solusi.” (Relayed dari Google’s dynamic-rendering doc; wording matches what’s sudah cited di ini situs’s broader JavaScript SEO guide.) ini adalah sebuah bridge, not sebuah architecture.
History API vs. hash routing (#!)
Five states, not two
Testing sebuah SPA route gets confusing because “does it work” (terjemahan) “melakukan ini berfungsi” actually spans five berbeda, separately-verifiable states:
| State | What ini adalah |
|---|---|
| server respons | bytes sebuah fresh, JS-free HTTP permintaan untuk sebuah URL actually gets back. |
| Rendered DOM | What sebuah browser (atau Googlebot’s renderer) membangun setelah executing JavaScript terhadap itu server respons. |
| Search processing | How Google separately melakukan crawl server respons, later renders halaman, dan indeks berdasarkan both. |
| browser full-document navigation | sebuah actual baru HTTP permintaan untuk sebuah URL — satu-satunya state itu dapat perubahan server respons atau kode status. |
| browser soft navigation | sebuah History API transition (pushState/replaceState) itu perubahan terlihat URL, browser history, dan pada-screen UI. |
one everyone conflates: sebuah soft navigation perubahan URL dan UI, tetapi ini melakukan
not oleh itself buat baru respons HTTP atau status — itu hanya happens pada sebuah full
navigation (atau sebuah equivalent direct permintaan, like curl). Testing sebuah route oleh clicking
melalui app dari homepage exercises soft navigation; testing ini oleh requesting
URL directly exercises server respons. Both penting, dan mereka dapat disagree.
What History API gives Anda
Google’s recommendation adalah unambiguous: “We recommend using the History API to load
different content based on the URL in a SPA.” (terjemahan) “kami recommend menggunakan History API untuk muat
berbeda konten berdasarkan URL di sebuah SPA.” (di live doc “History API” (terjemahan) “History API” adalah
sebuah tautan, so ini deep tautan targets lead-di clause.) History API
(pushState/replaceState) lets Anda router perubahan terlihat URL untuk sebuah nyata,
bookmarkable path — /products, not /#/products — without sebuah full reload. itu fixes
addressability half: setiap view now memiliki sebuah URL sebuah server dapat respond untuk
differently.
Why fragment/hash URLs adalah invisible untuk server — dan untuk Google
Because fragment tidak pernah reaches server (see above), History API adalah satu-satunya
cara untuk give setiap route sebuah URL server dapat actually sajikan. Google puts sebuah floor di bawah
ini di -nya basics doc: “don’t use fragments to load different page content. The
following example is a bad practice, because Googlebot can’t reliably resolve the
URLs.” (terjemahan) “don’t gunakan fragments untuk muat berbeda halaman konten.
following contoh adalah sebuah buruk practice, because Googlebot dapat’t reliably resolve
URLs.” ini adalah yang sama guidance I’ve written elsewhere — gunakan normal-looking URLs like
/products, not hash URLs like /#/products, because Google dapat’t reliably indeks
hash ones.
2015 deprecation, briefly
hash-bang (#!) era ended dengan Google’s 2015 post: “Times have changed. Today, as
long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are
generally able to render and understand your web pages like modern browsers,” (terjemahan) “Times memiliki changed. Today, sebagai
panjang sebagai Anda’re not blocking Googlebot dari crawling Anda JavaScript atau CSS files, kami adalah
umumnya able untuk render dan memahami Anda halaman web like modern browser,” dan
“you can use the History API pushState() to ensure accessibility for a wider range of
browsers (and our systems).” (terjemahan) “Anda dapat gunakan History API pushState() untuk ensure accessibility untuk sebuah wider range dari
browser (dan kami sistem).” One nuance worth keeping: Google didn’t instantly deindex
old hash-bang situs — “we’ll generally crawl, render, and index the #! URLs” (terjemahan) “kami’ll umumnya crawl, render, dan indeks undefined URLs” — tetapi
“we can still try” (terjemahan) “kami dapat masih try” adalah not “you should still do this.” (terjemahan) “Anda harus masih melakukan ini.”
membuat setiap route independently dapat diindeks
Clean URLs dan nyata HTML get Anda di-crawl. untuk get terindeks correctly, setiap route perlu -nya own signals.
Per-route tag canonical — dan “most restrictive directive” (terjemahan) “sebagian besar restrictive directive” trap
setiap route perlu -nya own rel=canonical di rendered DOM. trap: jika sebuah
placeholder canonical (atau sebuah noindex) ships di raw HTML shell dan JavaScript adalah
supposed untuk overwrite ini later, Anda dapat get sebuah conflict. Google resolves conflicts
antara raw dan rendered versi oleh taking more restrictive signal — sebagai I’ve
put ini sebelum, Google akan choose paling restrictive statements antara HTML dan
rendered versi dari sebuah halaman. sebuah stray noindex atau wrong canonical baked ke
shell dapat silently suppress whole route bahkan setelah Anda JS “fixes” (terjemahan) “fixes” ini.
Per-route judul dan deskripsi meta via JavaScript
Setting ini di JS adalah fine — Google says so directly: “You can use JavaScript to set
or change the meta description as well as the <title> element.” (terjemahan) “Anda dapat gunakan JavaScript untuk set
atau perubahan deskripsi meta serta undefined element.” requirement adalah
itu mereka land di rendered DOM Google evaluates, not hanya flash briefly. Give setiap
halaman -nya own judul dan deskripsi itu update when route perubahan.
nyata <a href> tautan antara routes
membuat Anda inter-route tautan nyata anchors dengan href attributes, not click handlers pada
<div>s. Google discovers URLs oleh extracting hrefs; sebuah <div onClick> itu navigates
via router adalah invisible untuk crawler’s tautan extraction. dan don’t lean pada
client-side state untuk carry konten di seluruh itu navigations — Google’s renderer “does
not retain state across page loads: Local Storage and Session Storage data are cleared
across page loads. HTTP Cookies are cleared across page loads.” (terjemahan) “melakukan
not retain state di seluruh halaman memuat: Local Storage dan Session Storage data adalah cleared
di seluruh halaman memuat. HTTP Cookies adalah cleared di seluruh halaman memuat.”
Sitemap generation untuk SPA routes
There’s no special “SPA sitemap” (terjemahan) “SPA sitemap” format — ini adalah sebuah accounting masalah
sitemap protocol adalah unchanged untuk SPAs. actual berfungsi adalah membuat sure setiap route Anda list independently resolves untuk nyata, unique, rendered HTML. sebuah sitemap dari 500 client-side routes adalah worthless jika itu routes semua mengembalikan sama shell. So sitemap adalah downstream dari Anda rendering strategy, not sebuah substitute untuk ini.
Handling dynamic/parameterized routes (/product/:id)
untuk apps dengan parameterized routes, Anda dapat’t hand-maintain list. sitemap
generator memiliki untuk run terhadap yang sama data source app menggunakan — sebuah bangun script atau sebuah
server endpoint itu enumerates setiap id — so sitemap dan app tidak pernah drift
apart. itu enumerated URLs adalah hanya worth listing once mereka’re SSR’d atau prerendered.
Keeping sitemap di sync dengan what’s server-resolvable
Regenerate sitemap sebagai bagian dari Anda bangun atau pada sebuah schedule tied untuk Anda konten
source. sebuah route itu 404s (atau worse, soft-404s di 200) tetapi sits di Anda sitemap adalah sebuah
crawl-budget dan quality signal Anda tidak ingin.
Testing what Google actually sees
Don’t spot-periksa one URL. whole poin dari SPA masalah adalah itu routes dapat differ di browser tetapi not pada wire, so test multiple routes di raw-HTML tingkat:
- Fetch raw HTML per route dengan
curl(sebuah Googlebot pengguna-agent where relevant) dan confirm konten adalah unique per URL, not shared shell. - pemeriksaan URL di Search Console — compare di-crawl/rendered HTML untuk several routes, dan confirm per-route judul, deskripsi, dan canonical adalah present.
- Confirm error routes mengembalikan right signal — sebuah “not found” (terjemahan) “tidak ditemukan” view seharusnya either
redirect untuk sebuah nyata error status atau carry
noindexdi rendered DOM.
umum myths tentang SPA SEO
- “Google can’t index SPAs at all.” (terjemahan) “Google dapat’t indeks SPAs di semua.” Outdated. Google runs evergreen Chromium dan umumnya renders SPA konten. nyata risks adalah spesifik: soft 404s, hash routing, render timeouts, dan non-Google crawler (Bing less reliably, sebagian besar AI crawler not di semua).
- “Adding the History API fixes SPA SEO.” (terjemahan) “menambahkan History API fixes SPA SEO.” ini fixes addressability hanya. jika server masih mengembalikan yang sama shell untuk setiap route, Google masih memiliki untuk execute JS untuk see anything.
- “Hash routing still works as a fallback.” (terjemahan) “Hash routing masih berfungsi sebagai sebuah fallback.” Deprecated since 2015 dan recommended terhadap ever since.
- “Client-side rendering is a ranking penalty.” (terjemahan) “rendering sisi klien adalah sebuah peringkat penalty.” No direct CSR penalty exists. damage adalah indirect — failed/delayed rendering, soft 404s, dan duplicate clustering dari render timeouts reduce what gets terindeks.
- “Pre-rendering for bots is cloaking.” (terjemahan) “Pre-rendering untuk bot adalah cloaking.” Not when konten matches what pengguna eventually see — hanya when/where dari rendering differs, not konten.
- “You need a special SPA sitemap generator.” (terjemahan) “Anda perlu sebuah special SPA sitemap generator.” No special format exists; berfungsi adalah membuat setiap listed route resolve untuk nyata HTML.
FAQs
dapat Google indeks sebuah single-halaman application? Yes, jika setiap route resolves untuk nyata, unique HTML (via SSR/prerendering) di sebuah nyata URL. Bare client-hanya SPAs sering don’t.
melakukan I perlu SSR, atau adalah rendering sisi klien ever okay? CSR dapat berfungsi untuk konten itu doesn’t perlu untuk peringkat, tetapi untuk anything Anda ingin terindeks reliably, prerender atau SSR ini — don’t bet pada renderer executing Anda JS di time.
adalah hash (#!) routing buruk untuk SEO? Yes — fragment tidak pernah reaches server, so
Google dapat’t reliably resolve itu URLs. gunakan History API.
melakukan setiap route perlu -nya own tag canonical? Yes, di rendered DOM — dan membuat
sure nothing more restrictive (sebuah stray noindex atau wrong canonical) ships di raw
shell.
adalah sebuah prerendering service cloaking? No, disediakan konten disajikan untuk bot matches what pengguna see.
seharusnya I gunakan meta-framework alih-alih membangun routing myself? untuk sebuah baru bangun, biasanya yes — Next.js/Nuxt/Angular-SSR/SvelteKit give Anda SSR/SSG dan per-route metadata untuk free.
Why melakukan my SPA kembalikan 200 untuk halaman itu don’t exist? Because client-side
router mempertahankan original 200 status untuk virtual navigations. Fix ini dengan sebuah JS
redirect untuk sebuah nyata error status atau sebuah rendered noindex.
AI summary
sebuah condensed take pada Advanced versi:
- SPA SEO = client-side routing specifically (React Router, Vue Router, Angular Router). sebuah SPA adalah defined oleh memuat one document dan swapping views dengan JavaScript — sebuah “app shell” (terjemahan) “app shell” itu mengirim near-empty HTML untuk setiap URL adalah one umum implementation, not sebuah aturan setiap SPA mengikuti; periksa what sebuah direct permintaan actually mengembalikan alih-alih assuming.
- ** core risk:** client-side routing dapat perubahan what pengguna sees without
changing what server akan kembalikan. pada sebuah bare app-shell SPA, fetching
/productsdan/aboutdirectly dapat get byte-identical raw HTML. - Five states get conflated: server respons, rendered DOM, Search processing, browser full-document navigation, dan browser soft navigation. sebuah History API transition perubahan URL dan UI tetapi doesn’t itself buat baru server respons atau status.
- Three symptoms dari sebuah bare shell: app-shell masalah, soft 404s (routers pertahankan sebuah
200untuk “not found” (terjemahan) “tidak ditemukan” — Google’s two documented fixes adalah redirecting untuk sebuah URL itu itself 404s, atau sebuah JS-ditambahkannoindex; sebuah initialnoindexdapat cause rendering untuk menjadi skipped), dan shared-shell duplicates — which “render timeout” (terjemahan) “render timeout” (Illyes) adalah one mungkin cause dari, not sebuah automatic diagnosis without permintaan/render/Search evidence. - Hash routing fails mechanically: fragment setelah
#tidak pernah reaches server, so server dapat’t differ oleh route. Deprecated oleh Google di 2015; History API gives addressability, not sebuah complete indexability fix. - ** fix memiliki two independent halves where sebuah bare shell adalah masalah:** addressability (History API, unique per-route URLs) dan konten availability (SSR, prerendering/SSG, atau sebuah meta-framework) — tetapi SSR isn’t sebuah universal requirement; base panggil pada whether route perlu untuk peringkat, what ini sudah mengembalikan directly, dan which crawler perlu untuk render ini.
- Per route: own canonical/judul/deskripsi di rendered DOM; watch
“most restrictive directive” (terjemahan) “sebagian besar restrictive directive” trap where sebuah raw-HTML
noindex/canonical overrides Anda JS. nyata<a href>tautan; don’t rely pada client state (WRS clears storage/cookies di seluruh memuat). - Sitemaps: listing sebuah route doesn’t prove ini resolves — no special format exists; setiap listed route harus independently resolve untuk nyata HTML, dan dynamic routes perlu sebuah generator tied untuk app’s data source.
- Dynamic rendering adalah sebuah Google/Bing-accepted stopgap, not sebuah panjang-istilah architecture.
- Test multiple routes di raw-HTML tingkat (direct permintaan), not hanya oleh navigating di dalam app.
Official documentation
Primary-source documentation dari mesin pencari.
- memahami JavaScript SEO basics — app-shell model, “Use the History API instead of fragments,” (terjemahan) “gunakan History API alih-alih fragments,” dan setting judul/deskripsi via JS.
- Fix Search-related JavaScript masalah — SPA soft-404 bagian, “Don’t use URL fragments to load different content,” (terjemahan) “Don’t gunakan URL fragments untuk muat berbeda konten,” dan History API recommendation.
- Dynamic rendering sebagai sebuah workaround — why dynamic rendering adalah sebuah stopgap, not sebuah panjang-istilah solusi.
- Deprecating kami AJAX crawling scheme (2015) — formal end dari hash-bang crawling dan pushState recommendation.
- bangun dan submit sebuah sitemap — umum sitemap protocol (there’s no SPA-spesifik format).
Bing / Microsoft
- bingbot Series: JavaScript, Dynamic rendering, dan Cloaking. Oh My! — bingbot’s JS-rendering capabilities dan -nya dynamic-rendering stance.
Quotes dari source
pada—record statements dari Google dan Bing. setiap tautan adalah sebuah deep tautan itu jumps untuk quoted passage pada source halaman.
Google — app-shell masalah
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (terjemahan) “beberapa JavaScript situs dapat gunakan app shell model where initial HTML melakukan not berisi actual konten dan Google perlu untuk execute JavaScript sebelum menjadi able untuk see actual halaman konten.” Jump untuk quote
Google — hash/fragment routing dan History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (terjemahan) “sebuah SPA dapat gunakan URL fragments (misalnya undefined untuk memuat berbeda views.” Jump untuk quote
- “We recommend using the History API to load different content based on the URL in a SPA.” (terjemahan) “kami recommend menggunakan History API untuk muat berbeda konten berdasarkan URL di sebuah SPA.” Jump untuk quote (di live doc “History API” (terjemahan) “History API” adalah sebuah tautan, so ini deep tautan targets lead-di clause; full kalimat adalah present verbatim di order.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (terjemahan) “don’t gunakan fragments untuk muat berbeda halaman konten. following contoh adalah sebuah buruk practice, because Googlebot dapat’t reliably resolve URLs.” Jump untuk quote
Google — SPA soft 404s
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (terjemahan) “di sebuah single-halaman application (SPA), ini dapat menjadi terutama difficult. untuk mencegah halaman error dari menjadi terindeks, Anda dapat gunakan one atau both dari following strategies.” Jump untuk quote
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (terjemahan) “When sebuah SPA adalah menggunakan client-side JavaScript untuk handle errors mereka sering report sebuah undefined HTTP kode status alih-alih appropriate kode status.” Jump untuk quote
Google — meta tags via JavaScript, dan stateless rendering
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (terjemahan) “Anda dapat gunakan JavaScript untuk set atau perubahan deskripsi meta serta undefined element.” Jump untuk quote - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (terjemahan) “WRS melakukan not retain state di seluruh halaman memuat: Local Storage dan Session Storage data adalah cleared di seluruh halaman memuat. HTTP Cookies adalah cleared di seluruh halaman memuat.” Jump untuk quote
Google — deprecating hash-bang crawling (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (terjemahan) “di pendek: kami adalah no longer recommending AJAX crawling proposal kami dibuat back di 2009.” Jump untuk quote
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (terjemahan) “Times memiliki changed. Today, sebagai panjang sebagai Anda’re not blocking Googlebot dari crawling Anda JavaScript atau CSS files, kami adalah umumnya able untuk render dan memahami Anda halaman web like modern browser.” Jump untuk quote
Google — dynamic rendering adalah sebuah workaround (relayed; wording matches what’s sudah cited di ini situs’s broader JavaScript SEO guide, not independently re-verified ini pass)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (terjemahan) “Dynamic rendering adalah sebuah workaround dan not sebuah panjang-istilah solusi untuk masalah dengan JavaScript-generated konten di mesin pencari.” Jump untuk quote
Bing — bingbot dan JavaScript
- “bingbot is generally able to render JavaScript.” (terjemahan) “bingbot adalah umumnya able untuk render JavaScript.” — bingbot Series, Bing Webmaster Blog. Read post
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (terjemahan) “bingbot melakukan not necessarily mendukung semua yang sama JavaScript frameworks itu adalah didukung di latest versi dari Anda favorite modern browser.” — sama post. Read post
Gary Illyes, Google — render timeouts buat duplicates (relayed via sebuah LinkedIn post, which resists automated verification; treat sebagai tinggi-confidence secondary)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (terjemahan) “ centerpiece took forever untuk muat, so rendering timed out… dan kami adalah left dengan sebuah bunch dari halaman itu hanya memiliki boilerplate. dengan hanya boilerplate, itu halaman adalah dups.” Read post
Which rendering path seharusnya I take?
berfungsi top-down. pertanyaan adalah selalu “does the crawler get real HTML for this route without executing my JS?” (terjemahan) “melakukan crawler get nyata HTML untuk ini route without executing my JS?” — dan start oleh confirming route actually perlu untuk menjadi terindeks dan memeriksa what sebuah direct, JS-free permintaan sudah mengembalikan; not setiap route perlu SSR, dan beberapa sudah kembalikan usable HTML.
1. adalah Anda starting sebuah baru bangun?
- Yes → gunakan meta-framework (Next.js, Nuxt, Angular dengan SSR, SvelteKit, Remix). SSR/SSG dan per-route metadata come dibangun di. Stop here.
- No → Continue.
2. melakukan Anda konten perubahan per pengguna / per permintaan?
- No (mostly static konten) → Prerender / SSG di bangun time. Simplest robust fix — setiap route adalah nyata HTML pada disk.
- Yes → rendering sisi server (SSR) so setiap permintaan mengembalikan route-spesifik HTML.
3. dapat’t melakukan SSR atau prerendering right now (legacy hand-rolled SPA)?
- gunakan dynamic rendering sebagai sebuah temporary bridge — sajikan crawler sebuah rendered snapshot. Plan migration untuk SSR/SSG; don’t treat ini sebagai destination.
4. Whatever path Anda chose, confirm semua dari ini per route:
- nyata URL via History API (no
#!fragments). - Unique judul + deskripsi meta + canonical di rendered DOM.
- Nothing more restrictive (
noindex, wrong canonical) baked ke raw shell. - nyata
<a href>tautan antara routes. - Error routes mengembalikan nyata error status atau sebuah rendered
noindex(no soft 404s). - setiap route di sitemap independently resolves untuk nyata HTML.
SPA SEO checklist
Addressability
- setiap view memiliki sebuah nyata URL via History API (
/products), not sebuah fragment (/#/products). - Inter-route tautan adalah nyata
<a href>anchors, not<div onClick>handlers.
konten availability
- setiap route mengembalikan nyata, unique HTML without crawler executing Anda JS (SSR, prerendering/SSG, atau sebuah meta-framework).
- Route konten memuat sebelum render window closes (hindari boilerplate-hanya duplicates).
Per-route indexability
- Unique
<title>dan deskripsi meta per route, present di rendered DOM. - Unique
rel=canonicalper route di rendered DOM. - No stray
noindex/placeholder canonical di raw shell itu sebuah more restrictive signal dapat lock di.
Errors
- “Not found” (terjemahan) “tidak ditemukan” routes redirect untuk sebuah URL whose server mengembalikan sebuah nyata error status atau
carry
noindexditambahkan oleh JavaScript (no soft 404s di200). - jika menggunakan
noindexroute, confirm ini adalah ditambahkan setelah app decides route adalah invalid, not present dari very pertama paint — sebuah initialnoindexdapat cause Google untuk skip rendering halaman.
Sitemap
- setiap listed route independently resolves untuk nyata HTML.
- Dynamic routes (
/product/:id) enumerated dari app’s data source, not hand-maintained.
Verification
- Raw HTML diperiksa di seluruh multiple routes (curl), confirming unique konten per URL.
- pemeriksaan URL confirms rendered konten, judul, deskripsi, dan canonical per route.
mental models
1. two halves Anda harus not conflate. Addressability (History API, unique URLs, no fragments) dan konten availability (SSR, prerendering, atau sebuah meta-framework) adalah independent. Clean URLs dengan no per-route HTML = sebuah tidy sitemap dari blank shells. Anda perlu both.
2. “What would the server return, fresh?” (terjemahan) “What akan server kembalikan, fresh?”
whole SPA masalah adalah itu browser view diverges dari server respons. untuk
apa pun route, tanyakan what sebuah curl dengan no JS execution gets back. jika ini adalah shell,
crawler dapat see shell too.
3. status-code default adalah 200 — including untuk errors.
Client-side routers pertahankan original 200. Assume setiap “error” (terjemahan) “error” view adalah sebuah soft 404
until Anda’ve deliberately dibuat ini mengembalikan nyata error status atau sebuah rendered noindex.
4. paling-restrictive-directive trap.
Google reconciles raw vs. rendered oleh taking more restrictive signal. sebuah noindex
atau wrong canonical di shell dapat override Anda JS “fix.” (terjemahan) “fix.” Audit raw HTML, not hanya
rendered DOM.
5. Meta-framework pertama untuk baru membangun. Hand-rolling client-side routing berarti owning setiap one dari ini masalah. Picking sebuah framework itu melakukan SSR/SSG dan per-route metadata adalah choosing not untuk memiliki them.
SPA SEO cheat sheet
Routing
| Approach | URL contoh | Reaches server? | Google-safe? |
|---|---|---|---|
| History API | /products | Yes | Yes (recommended) |
| Hash routing | /#/products | No (fragment tidak pernah dikirim) | No — deprecated 2015 |
Hash-bang (#!) | /#!/products | No | No — legacy AJAX scheme, deprecated |
rendering strategies
| Strategy | crawler gets nyata HTML without running JS? | Best untuk |
|---|---|---|
| SSR | Yes | Per-pengguna / per-permintaan konten |
| Prerendering / SSG | Yes | Mostly-static konten |
| Meta-framework (Next/Nuxt/etc.) | Yes (dibangun di) | baru membangun |
| Dynamic rendering | Yes, untuk bot hanya | Temporary bridge pada legacy SPAs |
| Bare CSR (client-hanya) | No | Nothing Anda ingin terindeks reliably |
Per-route harus-haves (semua di rendered DOM)
- Unique URL (History API) · unique
<title>· unique deskripsi meta · uniquerel=canonical· nyata<a href>tautan · error routes dengan sebuah nyata status ataunoindex.
Fast facts
- Fragment setelah
#adalah tidak pernah dikirim untuk server — itu’s why hash routing fails. - Client-side routers pertahankan
200untuk everything, including “not found” (terjemahan) “tidak ditemukan” → soft 404s. - Google reconciles raw vs. rendered oleh taking more restrictive signal.
- No special SPA sitemap format exists — setiap listed route harus resolve untuk nyata HTML.
SPA SEO anti-patterns
Shipping sebuah bare CSR SPA dan submitting sebuah full sitemap. sitemap lists 500 routes; semua 500 mengembalikan sama shell untuk sebuah fresh fetch. sitemap doesn’t membuat konten exist — rendering melakukan.
menambahkan History API dan calling ini done. Clean URLs fix addressability, not konten. Without SSR/prerendering server masih mengembalikan shell untuk setiap route.
Hash / hash-bang routing “as a fallback.” (terjemahan) “sebagai sebuah fallback.” fragment tidak pernah reaches server, so server dapat’t differ oleh route. Deprecated since 2015; not sebuah fallback, sebuah dead end.
Navigating dengan <div onClick> alih-alih <a href>.
Google extracts hrefs untuk menemukan URLs. Click-handler navigation adalah invisible untuk tautan
extraction, so itu routes dapat tidak pernah menjadi ditemukan.
sebuah placeholder noindex atau canonical di raw shell “that JS will overwrite.” (terjemahan) “itu JS akan overwrite.”
Google takes more restrictive dari raw vs. rendered. shell’s directive dapat win dan
silently suppress route.
Returning 200 untuk “not found” (terjemahan) “tidak ditemukan” views.
Soft 404s get thin/empty halaman terindeks. Redirect untuk sebuah nyata error status atau tambahkan sebuah
rendered noindex.
memuat route konten slowly, setelah boilerplate. Render timeouts leave hanya shared shell, dan setiap route collapses ke sebuah duplicate dari setiap lainnya (Illyes). muat centerpiece konten pertama.
Relying pada client state untuk carry konten di seluruh navigations. renderer clears Local/Session Storage dan cookies di seluruh halaman memuat — konten itu hanya exists di client state won’t menjadi there when Google renders next route.
umum SPA pengindeksan issues
setiap route mengembalikan yang sama HTML
Symptom: /products dan /about memiliki berbeda browser views tetapi identical raw
respons. mungkin cause: History API routing menyediakan addressability without SSR
atau prerendering. Fix: Produce route-spesifik HTML dan confirm sebuah direct permintaan untuk
setiap path berisi -nya own heading, body copy, dan metadata.
Missing routes appear sebagai successful halaman
Symptom: sebuah nonexistent route menampilkan sebuah not-ditemukan view while returning 200.
mungkin cause: client router owns error setelah server memiliki sudah dikirim sebuah
successful shell. Fix: mengembalikan right server status; jika itu cannot ship yet,
redirect untuk sebuah URL dengan sebuah nyata error status atau render noindex. Confirm dengan sebuah fresh
direct permintaan, not sebuah di-app navigation.
Routes collapse ke duplicates setelah rendering
Symptom: mesin pencari cluster distinct URLs atau retain hanya shared navigation. mungkin cause: Route konten memuat too late dan rendering captures boilerplate. Fix: Prioritize primary konten di server respons atau earliest render path. Confirm several routes expose unique konten sebelum optional scripts finish.
Testing what server actually mengembalikan per route
SPA trap adalah itu routes differ di browser tetapi not pada wire. periksa multiple routes di raw-HTML tingkat, not one.
Fetch raw HTML per route (macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
doneDiff two routes’ raw HTML (adalah mereka yang sama shell?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"periksa per-route judul dan canonical di raw respons (grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'periksa HTTP status dari sebuah “not found” (terjemahan) “tidak ditemukan” route (soft-404 detector)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existdi browser DevTools console — inspect rendered judul/canonical
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);Bookmarklet — flag click-handler “links” (terjemahan) “tautan” itu aren’t nyata anchors
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath — temukan fragment/hash tautan sebuah crawler dapat’t resolve (paste ke DevTools console)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. alat untuk auditing sebuah SPA
- pemeriksaan URL (Google Search Console) — see di-crawl vs. rendered HTML untuk sebuah route dan confirm per-route judul, deskripsi, dan canonical actually land.
- Rich hasil Test / pemeriksaan URL “View crawled page” (terjemahan) “View di-crawl halaman” — Google’s own render dari sebuah single URL, berguna untuk confirming konten survives rendering.
curldengan sebuah Googlebot UA — fastest cara untuk compare raw HTML dari several routes dan catch sebuah shared shell.- Screaming Frog SEO Spider (JavaScript rendering mode) — crawl situs dengan dan without rendering untuk compare raw vs. rendered konten dan judul di seluruh setiap route.
- Ahrefs situs Audit — surfaces indexability issues, missing/duplicate judul dan canonicals, dan soft-404-like patterns di seluruh routes.
- DebugBear — SPA-oriented performa monitoring; render timing penting because slow routes dapat time out ke boilerplate duplicates.
- Bing Webmaster alat — pemeriksaan URL — bingbot renders JS less consistently daripada Googlebot, so confirm Anda routes pada Bing’s side too.
Prove sebuah SPA route adalah independently dapat diindeks
Test direct-entry HTML parity
Test untuk run: Open representative routes di sebuah baru session dan fetch yang sama URLs
dengan curl. Expected hasil: setiap URL mengembalikan -nya own primary konten dan head
signals without prior app state. Failure interpretation: route depends pada
client navigation atau stored state. Monitoring window: Immediate. Rollback
trigger: sebuah route berfungsi hanya setelah entering melalui homepage.
Test error status handling
Test untuk run: permintaan sebuah known-invalid route directly dan inspect both status dan
rendered directives. Expected hasil: sebuah genuine error status, atau documented
fallback dari sebuah rendered noindex/redirect untuk sebuah error respons. Failure
interpretation: SPA adalah generating soft 404s. Monitoring window: Immediate.
Rollback trigger: Invalid paths ship sebagai dapat diindeks 200 halaman.
Test metadata isolation
Test untuk run: Compare raw dan rendered judul, robots tag, dan canonical di seluruh setidaknya three routes. Expected hasil: setiap route memiliki one intended, internally consistent set. Failure interpretation: shared shell leaks metadata antara routes atau JS overwrites ini too late. Monitoring window: Immediate locally dan setelah recrawl di pemeriksaan URL. Rollback trigger: apa pun route inherits lainnya route’s canonical atau sebuah restrictive shell directive.
Test yourself: SPA SEO
Five quick pertanyaan pada membuat single-halaman applications dapat di-crawl dan dapat diindeks. Pick sebuah jawaban untuk setiap, lalu periksa.
Resources worth Anda time
My related writing
- JavaScript SEO Issues & Best Practices — my full JS-SEO guide, including “don’t use fragments in URLs” (terjemahan) “don’t gunakan fragments di URLs” dan app-shell/duplicate-konten bagian itu ini artikel membangun pada.
- Beginner’s Guide untuk SEO teknis — where rendering dan crawling fit di bigger picture.
My speaking
- How Search berfungsi (SlideShare) — my walkthrough dari crawling, rendering, pengindeksan, dan peringkat, which adalah pipeline sebuah SPA memiliki untuk survive. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.” (terjemahan) “ini adalah my understanding dari sistem… not going untuk menjadi 100% complete atau accurate.”)
dari sekitar industry
- Google — memahami JavaScript SEO basics — app-shell model dan “Use the History API instead of fragments.” (terjemahan) “gunakan History API alih-alih fragments.”
- Google — Fix Search-related JavaScript masalah — SPA soft-404 bagian dan History API recommendation.
- Google — Deprecating kami AJAX crawling scheme (2015) — formal end dari hash-bang crawling.
- Bing — bingbot Series: JavaScript, Dynamic rendering, dan Cloaking. Oh My! — bingbot’s JS-rendering stance.
- ini adalah Not Cloaking untuk gunakan Pre-Render Service untuk Blank HTML halaman untuk SPA Development (mesin pencari Roundtable, 2015) — coverage dari Gary Illyes pada pre-rendering SPAs not menjadi cloaking. (SER’s paraphrase dari Illyes, dated 2015 — secondary.)
- SEO untuk Single halaman Applications (Nuxt SEO) — sebuah framework-side walkthrough dari yang sama masalah.
- cara mengoptimalkan Single halaman Applications untuk SEO (DebugBear) — rendering/performa angle pada SPA indexability.
- SPA (glossary) (MDN) — sebuah neutral definition dari single-halaman-application pattern.
Videos
- Google Search Central (YouTube) — Martin Splitt’s JavaScript SEO series covers rendering, History API, dan SPA failure modes discussed here. Channel
Log perubahan
Diperbarui 18 Jul 2026.
Ringkasan editorial dan detail perubahan yang tercatat.Detail perubahan
-
Catatan perubahan terperinci saat ini tersedia dalam bahasa Inggris.
-
Catatan perubahan terperinci saat ini tersedia dalam bahasa Inggris.
-
Catatan perubahan terperinci saat ini tersedia dalam bahasa Inggris.
-
Catatan perubahan terperinci saat ini tersedia dalam bahasa Inggris.
-
Catatan perubahan terperinci saat ini tersedia dalam bahasa Inggris.
Perbandingan lengkap tidak tersedia — tidak ada cuplikan sebelumnya yang diarsipkan untuk revisi ini.