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 melalui SSR/prerendering, per-route canonicals dan judul, dan sitemap generation untuk client-side routes.
sebuah single-halaman application memuat satu document dan swaps views dengan JavaScript alih-alih requesting sebuah baru halaman dari server. banyak SPAs implement itu dengan sebuah 'app shell' — server mengembalikan satu URL's worth dari nyata HTML, sebuah near-empty shell, until JS renders setiap route — tetapi itu's sebuah umum implementation pilihan, tidak sebuah aturan setiap SPA mengikuti; periksa apa sebuah direct permintaan untuk setiap route sebenarnya mengembalikan sebelum assuming ini. perbaiki, di mana sebuah bare app-shell setup adalah masalah, memiliki dua independent halves: addressability (History API, tidak hash/#! fragments, so setiap view memiliki sebuah nyata URL — sebuah browser soft navigation melalui History API perubahan URL dan UI tetapi tidak itself buat baru server respons) dan konten availability (SSR, prerendering, atau sebuah meta-kerangka kerja so setiap dari itu URLs dapat kembalikan unique HTML pada permintaan). pada top dari itu, verify setiap route's judul, canonical, dan robots state keduanya pada direct entry dan setelah rendering, tangani soft 404s (routers tend untuk pertahankan sebuah 200 status untuk 'tidak ditemukan' views — redirect untuk sebuah URL itu itself 404s, atau tambahkan sebuah dirender noindex, though sebuah awal noindex dapat penyebab rendering untuk menjadi skipped), dan pastikan setiap route di Anda sitemap sebenarnya resolves untuk nyata, dapat diindeks HTML — ada Tidak special SPA sitemap format.
TL;DR — sebuah single-halaman application (SPA) memuat satu halaman dari server dan lalu menggunakan JavaScript untuk switch “halaman” (terjemahan) “halaman” tanpa sebuah penuh reload. catch: banyak SPAs kirim yang sama near-empty starting halaman Tidak penting yang URL sebuah mesin pencari menanyakan untuk — itu’s sebuah umum risk dari bagaimana SPAs adalah biasanya dibangun, tidak sebuah jaminan dari setiap SPA. untuk perbaiki ini, pastikan setiap route memiliki -nya own nyata URL dan -nya own nyata HTML — biasanya oleh rendering halaman pada server atau pre-membangun them.
Apa sebuah SPA adalah
sebuah single-halaman application commonly memperbarui client-side views dan routes tanpa sebuah penuh document navigation. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: MDN: Single-page application Google dapat render JavaScript SPAs, tetapi dapat di-crawl URLs, tautan, status menangani, dan dirender konten tetap necessary. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: Google: JavaScript SEO basics
sebuah single-halaman application adalah sebuah situs web dibangun sebagai satu HTML halaman. Ketika Anda click sekitar — dari products halaman untuk tentang halaman — JavaScript swaps out apa pada screen alih-alih asking server untuk sebuah seluruh baru halaman. React (dengan React Router), Vue (dengan Vue Router), dan Angular semua berfungsi ini cara oleh default. ini feels fast dan app-like, yang adalah mengapa ini adalah popular.
risk adalah apa 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 mendapatkan dibangun di browser afterward. itu’s
satu umum cara untuk bangun sebuah SPA — tidak definition dari satu, dan tidak sesuatu setiap
SPA melakukan. tetapi ketika sebuah situs melakukan ship sebuah bare app shell, failure mode adalah nyata: jika
Google menanyakan server untuk /about dan /products secara terpisah, ini dapat mendapatkan tepat
sama blank shell untuk keduanya, karena tidak ada apa pun tentang route changed apa server
dikirim. cara untuk know apakah Anda app adalah di itu position adalah untuk periksa apa sebuah direct
permintaan untuk setiap route sebenarnya mengembalikan — tidak untuk assume ini dari fact itu ini adalah sebuah
SPA.
Mengapa itu hurts SEO
mesin pencari perlu untuk see Anda konten untuk peringkat ini. dengan sebuah bare SPA, three hal 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 “tidak ditemukan” (terjemahan) “tidak ditemukan” screen sementara 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 perbaiki ini ( pendek versi)
- Memberikan setiap view sebuah nyata URL menggunakan browser’s History API (bersih paths like
/products), tidak#-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 kerangka kerja itu melakukan ini untuk Anda — Berikutnya.js, Nuxt, SvelteKit, Angular’s SSR — saves Anda trouble.
- Memberikan setiap halaman -nya own judul dan deskripsi itu perubahan ketika route perubahan.
- membuat tautan nyata tautan (
<a href>), tidak clickable<div>s.
ingin mechanics — mengapa 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 tidak “JavaScript” (terjemahan) “JavaScript” di abstract — ini adalah itu client-side routing perubahan apa pengguna sees tanpa mengubah apa server akan kembalikan. perbaiki memiliki dua independent halves orang constantly conflate: addressability (History API, unique per-route URLs, Tidak
#!fragments) dan konten availability (SSR, prerendering/SSG, atau sebuah meta-kerangka kerja). melakukan hanya pertama dan Anda mendapatkan sebuah tidy sitemap dari URLs itu semua render yang sama shell. pada top dari keduanya, setiap route perlu -nya own canonical/judul/deskripsi di dirender DOM, Anda memiliki untuk tangani soft 404s itu pertahankan sebuah200, dan setiap route di sitemap memiliki untuk independently resolve untuk nyata HTML.
Apa SPA SEO sebenarnya adalah
SPA adalah sebuah application architecture, tidak sebuah guaranteed SEO failure state. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: MDN: Single-page application server rendering, prerendering, atau dengan hati-hati implemented client rendering dapat expose konten, tetapi none jaminan pengindeksan. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: Google: JavaScript SEO basics
ini adalah deep dive pada single-halaman applications secara khusus — client-side routing dengan React Router, Vue Router, atau Angular Router, di mana setelah pertama muat server tidak pernah mengirim lainnya penuh halaman. ini sits alongside lebih luas JavaScript SEO guide (yang covers parity, lazy-memuat, infinite scroll, dan JS rendering umumnya) dan per-kerangka kerja tulis-ups untuk React, Berikutnya.js, Nuxt, Angular, Vue, Svelte, dan Astro. Di sini I’m hanya interested di routing layer dan apa ini melakukan untuk crawling dan pengindeksan.
Mengapa bare client-side-routed SPAs fail di SEO
Satu URL, satu HTML respons — app-shell masalah
«Google describes the 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.” In a bare
SPA, the “app shell” is all the server ever returns. Fetch /products fresh and you
get the same near-empty document you’d get for /about. The content only diverges
after the browser runs your JavaScript and your router decides what to show.
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian dua puluh dua, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
itu’s seluruh masalah di satu kalimat: client-side routing perubahan apa pengguna sees tanpa mengubah apa server akan kembalikan. Semuanya downstream — soft 404s, duplicate konten, missing judul — adalah sebuah symptom dari itu.
server tidak pernah sees yang “halaman” (terjemahan) “halaman” adalah requested (mengapa hash routing fails)
Older SPAs muat views dari URL fragments — example.com/#/products. Google’s tepat
kata: “A SPA mungkin gunakan URL fragments (misalnya https://example.com/#/products) untuk
loading berbeda views.” (terjemahan) “sebuah SPA dapat gunakan URL fragments (misalnya tidak terdefinisi untuk
memuat berbeda views.” ini fails untuk sebuah spesifik, mechanical alasan: browser
tidak pernah kirim fragment (apa pun setelah #) untuk server di HTTP permintaan.
server secara harfiah cannot know yang “halaman” (terjemahan) “halaman” adalah requested, so ini dapat’t kembalikan berbeda
konten atau sebuah berbeda kode status untuk ini. sebuah crawler itu fetches URL setelah mendapatkan
identical HTML regardless dari fragment.
itu’s juga mengapa Google formally deprecated -nya 2009 AJAX-crawling scheme back di
October 2015: “Di short: We adalah Tidak lebih lama recommending itu AJAX crawling proposal we
membuat back di 2009.” (terjemahan) “di pendek: kami adalah Tidak lebih lama 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
memperbaiki ini. Google’s own recommendation replacing ini adalah History API, covered di bawah.
Soft 404s — client-side routers pertahankan sebuah 200 untuk semuanya
«This one is close to inevitable in a bare SPA. Client-side routers, by design, keep the
original page’s 200 status for every virtual navigation — including “not found”
states. Google flags it 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.” And the 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) (Ringkasan bahasa Indonesia untuk bagian dua puluh delapan, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
hasil adalah empty atau error views getting terindeks sebagai thin 200 halaman. Google
documents persis dua strategies di sini, precisely: redirect (atau membuat sebuah penuh permintaan) untuk
sebuah URL milik siapa server mengembalikan sebuah nyata 404/error status, atau tambahkan sebuah noindex tag untuk
error view dengan JavaScript. Watch kedua satu: jika itu noindex adalah present dari
very pertama paint alih-alih ditambahkan setelah app decides route adalah tidak valid, ini dapat
penyebab Google untuk skip rendering halaman altogether — so verify apa sebuah direct permintaan
mengembalikan sebelum apa pun JS berjalan, tidak hanya apa menampilkan up di dirender DOM afterward.
Shared-shell duplicates — sebuah mungkin penyebab, tidak sebuah automatic diagnosis
«There’s a second, sneakier failure mode: distinct routes get indexed as duplicates of each other because they render down to the same shared header/nav/footer boilerplate. Gary Illyes described one way this 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.” His fix: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (Relayed via a LinkedIn post, which resists automated verification; treat as high-confidence secondary.) » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian tiga puluh satu, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
«Notice Illyes frames it as “my most likely explanation,” not a confirmed diagnosis — that’s worth taking literally. “Render timeout” isn’t something you can diagnose from symptoms alone; you need request-level evidence (what a direct fetch actually returns), rendered-output evidence (whether the centerpiece content is present after rendering), and Search Console evidence (duplicate/indexing signals) before attributing a route’s duplicate-content problem to a timeout specifically, rather than to a bug, a blocked resource, or a genuinely identical shell. There’s also no fixed, published timeout to design around — Google’s own basics doc says a page “may stay on this queue for a few seconds, but it can take longer than that,” without committing to a number. Don’t plan around an assumed render-queue duration; instead, make sure primary content loads as early as possible regardless of how long rendering ends up taking. » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian tiga puluh dua, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
perbaiki: mendapatkan nyata HTML per route
penuh perbaiki memiliki dua independent bagian itu orang conflate constantly:
- URL addressability — History API, unique per-route URLs, Tidak fragments.
- konten availability — SSR, static prerendering/SSG, atau sebuah meta-kerangka kerja.
melakukan hanya #1 dan Anda mendapatkan bersih, shareable URLs itu semua masih mengembalikan sama blank shell. untuk sebuah route Anda sebenarnya ingin terindeks, Anda perlu keduanya.
itu’s tidak sebuah universal mandate untuk tambahkan SSR di mana-mana, though. SSR dan prerendering reduce bagaimana banyak Anda bergantung pada sebuah crawler successfully executing Anda JavaScript — mereka jangan become mandatory instant sebuah situs adalah technically sebuah SPA. sebelum picking sebuah architecture untuk sebuah diberikan route, periksa three hal: apakah itu route perlu untuk peringkat di semua (sebuah internal admin panel tidak), apa sebuah direct, JS-free permintaan untuk ini sudah mengembalikan (beberapa setups sudah kirim bermakna HTML), dan yang crawler Anda sebenarnya perlu untuk satisfy (Google renders JS fairly reliably; Bing dan sebagian besar AI crawler lebih sedikit so — see di bawah). CSR itu sudah melewati itu memeriksa tidak perlu untuk become SSR hanya karena situs adalah sebuah SPA.
rendering sisi server (SSR)
server berjalan Anda app untuk setiap permintaan dan mengembalikan fully-formed HTML untuk itu route, lalu client “hydrates” (terjemahan) “hydrates” ini ke sebuah langsung SPA. ini adalah paling robust option karena crawler mendapatkan menyelesaikan konten pada pertama fetch, Tidak 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 tidak 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 penelusuran mesin — hal untuk hindari adalah leaving konten locked behind client-hanya rendering.
atau: jangan hand-roll ini — gunakan meta-kerangka kerja
untuk sebuah baru bangun, honest recommendation adalah untuk tidak hand-roll React-Router-hanya atau Vue-Router-hanya client-side routing di semua. sebuah meta-kerangka kerja — Berikutnya.js, Nuxt, Angular dengan -nya SSR package, SvelteKit, Remix — memberikan Anda SSR/SSG dan route-based metadata out dari box, yang 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, tidak sebuah config toggle.
Dynamic rendering sebagai sebuah stopgap, tidak sebuah destination
«You can serve crawlers a separately-rendered HTML snapshot (dynamic rendering). Both Google and Bing accept it — Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (relayed from Bing’s 2018 blog; the two short bingbot-capability quotes below are directly verified, this longer one is not independently re-checked) — but Google is clear it’s a 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.” (Relayed from Google’s dynamic-rendering doc; wording matches what’s already cited in this site’s broader JavaScript SEO guide.) It’s a bridge, not an architecture. » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian empat puluh lima, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
«## History API vs. hash routing (#!)
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian empat puluh enam, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Five status, tidak dua
Testing sebuah SPA route mendapatkan confusing karena “melakukan it berfungsi” (terjemahan) “melakukan ini berfungsi” sebenarnya spans five berbeda, secara terpisah-verifiable status:
| State | Apa ini adalah |
|---|---|
| server respons | bytes sebuah fresh, JS-free HTTP permintaan untuk sebuah URL sebenarnya mendapatkan back. |
| Dirender DOM | Apa sebuah browser (atau Googlebot’s renderer) membangun setelah executing JavaScript terhadap itu server respons. |
| Penelusuran processing | Bagaimana Google secara terpisah melakukan crawl server respons, kemudian renders halaman, dan indeks berdasarkan keduanya. |
| browser penuh-document navigation | sebuah sebenarnya 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. |
satu everyone conflates: sebuah soft navigation perubahan URL dan UI, tetapi ini melakukan
tidak oleh itself buat baru respons HTTP atau status — itu hanya happens pada sebuah penuh
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 secara langsung exercises server respons. Keduanya penting, dan mereka dapat disagree.
Apa History API memberikan Anda
Google’s recommendation adalah unambiguous: “We merekomendasikan menggunakan itu History API untuk load
berbeda konten based pada itu URL di a SPA.” (terjemahan) “kami merekomendasikan menggunakan History API untuk muat
berbeda konten berdasarkan URL di sebuah SPA.” (di langsung 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, tidak /#/products — tanpa sebuah penuh reload. itu memperbaiki
addressability half: setiap view now memiliki sebuah URL sebuah server dapat respond untuk
differently.
Mengapa fragment/hash URLs adalah invisible untuk server — dan untuk Google
«Because the fragment never reaches the server (see above), the History API is the only
way to give each route a URL the server can actually serve. Google puts a floor under
it in its 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.” This is the same guidance I’ve written elsewhere — use normal-looking URLs like
/products, not hash URLs like /#/products, because Google can’t reliably index the
hash ones.
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian lima puluh empat, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
2015 deprecation, briefly
«The hash-bang (#!) era ended with 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,” and
“you can use the History API pushState() to ensure accessibility for a wider range of
browsers (and our systems).” One nuance worth keeping: Google didn’t instantly deindex
old hash-bang sites — “we’ll generally crawl, render, and index the #! URLs” — but
“we can still try” is not “you should still do this.”
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian lima puluh enam, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
membuat setiap route independently dapat diindeks
Bersih URLs dan nyata HTML mendapatkan Anda di-crawl. untuk mendapatkan terindeks correctly, setiap route perlu -nya own sinyal.
Per-route tag canonical — dan “most restrictive directive” (terjemahan) “sebagian besar restrictive directive” trap
«Every route needs its own rel=canonical in the rendered DOM. The trap: if a
placeholder canonical (or a noindex) ships in the raw HTML shell and JavaScript is
supposed to overwrite it later, you can get a conflict. Google resolves conflicts
between the raw and rendered versions by taking the more restrictive signal — as I’ve
put it before, Google will choose the most restrictive statements between the HTML and
the rendered version of a page. A stray noindex or wrong canonical baked into the
shell can silently suppress the whole route even after your JS “fixes” it.
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian enam puluh, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Per-route judul dan deskripsi meta melalui JavaScript
«Setting these in JS is fine — Google says so directly: “You can use JavaScript to set
or change the meta description as well as the <title> element.” The requirement is
that they land in the rendered DOM Google evaluates, not just flash briefly. Give each
page its own title and description that update when the route changes.
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian enam puluh dua, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
nyata <a href> tautan antara routes
«Make your inter-route links real anchors with href attributes, not click handlers on
<div>s. Google discovers URLs by extracting hrefs; a <div onClick> that navigates
via the router is invisible to the crawler’s link extraction. And don’t lean on
client-side state to carry content across those 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) (Ringkasan bahasa Indonesia untuk bagian enam puluh empat, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Sitemap generation untuk SPA routes
ada Tidak special “SPA sitemap” (terjemahan) “SPA sitemap” format — ini adalah sebuah accounting masalah
sitemap protocol adalah unchanged untuk SPAs. sebenarnya berfungsi adalah membuat sure setiap route Anda list independently resolves untuk nyata, unique, dirender 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, tidak sebuah substitute untuk ini.
Menangani dynamic/parameterized routes (/product/:id)
untuk apps dengan parameterized routes, Anda dapat’t hand-maintain list. sitemap
generator memiliki untuk jalankan terhadap yang sama data sumber 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 setelah mereka’re SSR’d atau prerendered.
Mempertahankan sitemap di sync dengan apa server-resolvable
Regenerate sitemap sebagai bagian dari Anda bangun atau pada sebuah schedule tied untuk Anda konten
sumber. sebuah route itu 404s (atau worse, soft-404s di 200) tetapi sits di Anda sitemap adalah sebuah
crawl-budget dan quality sinyal Anda tidak ingin.
Testing apa Google sebenarnya sees
jangan spot-periksa satu URL. seluruh poin dari SPA masalah adalah itu routes dapat differ di browser tetapi tidak pada wire, so test multiple routes di mentah-HTML tingkat:
- Fetch mentah HTML per route dengan
curl(sebuah Googlebot pengguna-agent di mana relevant) dan konfirmasi konten adalah unique per URL, tidak shared shell. - pemeriksaan URL di Search Console — bandingkan di-crawl/dirender HTML untuk several routes, dan konfirmasi per-route judul, deskripsi, dan canonical adalah present.
- Konfirmasi error routes mengembalikan right sinyal — sebuah “tidak ditemukan” (terjemahan) “tidak ditemukan” view seharusnya either
redirect untuk sebuah nyata error status atau carry
noindexdi dirender DOM.
umum myths tentang SPA SEO
- “Google tidak dapat index SPAs di all.” (terjemahan) “Google dapat’t indeks SPAs di semua.” Outdated. Google berjalan evergreen Chromium dan umumnya renders SPA konten. nyata risks adalah spesifik: soft 404s, hash routing, render timeouts, dan non-Google crawler (Bing lebih sedikit reliably, sebagian besar AI crawler tidak di semua).
- “Adding itu History API memperbaiki SPA SEO.” (terjemahan) “menambahkan History API memperbaiki SPA SEO.” ini memperbaiki addressability hanya. jika server masih mengembalikan yang sama shell untuk setiap route, Google masih memiliki untuk execute JS untuk see apa pun.
- “Hash routing masih berfungsi sebagai a fallback.” (terjemahan) “Hash routing masih berfungsi sebagai sebuah fallback.” Deprecated since 2015 dan recommended terhadap ever since.
- “Client-side rendering adalah a ranking penalty.” (terjemahan) “rendering sisi klien adalah sebuah peringkat penalty.” Tidak direct CSR penalty ada. damage adalah indirect — failed/delayed rendering, soft 404s, dan duplicate clustering dari render timeouts reduce apa mendapatkan terindeks.
- “Pre-rendering untuk bots adalah cloaking.” (terjemahan) “Pre-rendering untuk bot adalah cloaking.” Tidak ketika konten matches apa pengguna eventually see — hanya ketika/di mana dari rendering differs, tidak konten.
- “Anda perlu a special SPA sitemap generator.” (terjemahan) “Anda perlu sebuah special SPA sitemap generator.” Tidak special format ada; berfungsi adalah membuat setiap listed route resolve untuk nyata HTML.
FAQs
dapat Google indeks sebuah single-halaman application? Ya, jika setiap route resolves untuk nyata, unique HTML (melalui SSR/prerendering) di sebuah nyata URL. Bare client-hanya SPAs sering jangan.
melakukan I perlu SSR, atau adalah rendering sisi klien ever okay? CSR dapat berfungsi untuk konten itu tidak perlu untuk peringkat, tetapi untuk apa pun Anda ingin terindeks reliably, prerender atau SSR ini — jangan bet pada renderer executing Anda JS di time.
adalah hash (#!) routing buruk untuk SEO? Ya — fragment tidak pernah reaches server, so
Google dapat’t reliably resolve itu URLs. gunakan History API.
melakukan setiap route perlu -nya own tag canonical? Ya, di dirender DOM — dan membuat
sure tidak ada apa pun lebih restrictive (sebuah stray noindex atau wrong canonical) ships di mentah
shell.
adalah sebuah prerendering service cloaking? Tidak, disediakan konten disajikan untuk bot matches apa pengguna see.
seharusnya I gunakan meta-kerangka kerja alih-alih membangun routing myself? untuk sebuah baru bangun, biasanya Ya — Berikutnya.js/Nuxt/Angular-SSR/SvelteKit memberikan Anda SSR/SSG dan per-route metadata untuk free.
Mengapa melakukan my SPA kembalikan 200 untuk halaman itu jangan exist? Karena client-side
router mempertahankan original 200 status untuk virtual navigations. Perbaiki ini dengan sebuah JS
redirect untuk sebuah nyata error status atau sebuah dirender noindex.
AI summary
sebuah condensed take pada Advanced versi:
- SPA SEO = client-side routing secara khusus (React Router, Vue Router, Angular Router). sebuah SPA adalah didefinisikan oleh memuat satu document dan swapping views dengan JavaScript — sebuah “app shell” (terjemahan) “app shell” itu mengirim near-empty HTML untuk setiap URL adalah satu umum implementation, tidak sebuah aturan setiap SPA mengikuti; periksa apa sebuah direct permintaan sebenarnya mengembalikan alih-alih assuming.
- ** core risk:** client-side routing dapat perubahan apa pengguna sees tanpa
mengubah apa server akan kembalikan. pada sebuah bare app-shell SPA, fetching
/productsdan/aboutsecara langsung dapat mendapatkan byte-identical mentah HTML. - Five status mendapatkan conflated: server respons, dirender DOM, Penelusuran processing, browser penuh-document navigation, dan browser soft navigation. sebuah History API transition perubahan URL dan UI tetapi tidak itself buat baru server respons atau status.
- Three symptoms dari sebuah bare shell: app-shell masalah, soft 404s (routers pertahankan sebuah
200untuk “tidak ditemukan” (terjemahan) “tidak ditemukan” — Google’s dua terdokumentasi memperbaiki adalah mengalihkan untuk sebuah URL itu itself 404s, atau sebuah JS-ditambahkannoindex; sebuah awalnoindexdapat penyebab rendering untuk menjadi skipped), dan shared-shell duplicates — yang “render timeout” (terjemahan) “render timeout” (Illyes) adalah satu mungkin penyebab dari, tidak sebuah automatic diagnosis tanpa permintaan/render/Penelusuran 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 memberikan addressability, tidak sebuah menyelesaikan indexability perbaiki. - ** perbaiki memiliki dua independent halves di mana sebuah bare shell adalah masalah:** addressability (History API, unique per-route URLs) dan konten availability (SSR, prerendering/SSG, atau sebuah meta-kerangka kerja) — tetapi SSR tidak sebuah universal requirement; base panggil pada apakah route perlu untuk peringkat, apa ini sudah mengembalikan secara langsung, dan yang crawler perlu untuk render ini.
- Per route: own canonical/judul/deskripsi di dirender DOM; watch
“most restrictive directive” (terjemahan) “sebagian besar restrictive directive” trap di mana sebuah mentah-HTML
noindex/canonical overrides Anda JS. nyata<a href>tautan; jangan rely pada client state (WRS clears storage/cookies di seluruh memuat). - Sitemaps: listing sebuah route tidak prove ini resolves — Tidak special format ada; setiap listed route harus independently resolve untuk nyata HTML, dan dynamic routes perlu sebuah generator tied untuk app’s data sumber.
- Dynamic rendering adalah sebuah Google/Bing-accepted stopgap, tidak sebuah panjang-istilah architecture.
- Test multiple routes di mentah-HTML tingkat (direct permintaan), tidak hanya oleh navigating di dalam app.
Dokumentasi resmi
Utama-sumber 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 melalui JS.
- Perbaiki Penelusuran-related JavaScript masalah — SPA soft-404 bagian, “jangan gunakan URL fragments untuk load berbeda konten,” (terjemahan) “jangan gunakan URL fragments untuk muat berbeda konten,” dan History API recommendation.
- Dynamic rendering sebagai sebuah workaround — mengapa dynamic rendering adalah sebuah stopgap, tidak 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 (ada Tidak 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 sumber
pada—record statements dari Google dan Bing. setiap tautan adalah sebuah deep tautan itu jumps untuk quoted passage pada sumber 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.” Jump to quote » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus dua, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Google — hash/fragment routing dan History API
«- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” Jump to quote
- “We recommend using the History API to load different content based on the URL in a SPA.” Jump to quote (In the live doc “History API” is a link, so this deep link targets the lead-in clause; the full sentence is present verbatim in 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.” Jump to quote » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus empat, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Google — SPA soft 404s
- “Di a single-halaman application (SPA), ini dapat menjadi especially difficult. Untuk cegah error halaman dari menjadi indexed, Anda dapat gunakan satu atau keduanya dari itu berikut strategies.” (terjemahan) “di sebuah single-halaman application (SPA), ini dapat menjadi terutama difficult. untuk mencegah halaman error dari menjadi terindeks, Anda dapat gunakan satu atau keduanya dari berikut strategies.” Jump untuk quote
- “Ketika a SPA adalah menggunakan client-side JavaScript untuk tangani errors they sering report a
200HTTP status code alih-alih dari itu appropriate status code.” (terjemahan) “Ketika sebuah SPA adalah menggunakan client-side JavaScript untuk tangani errors mereka sering report sebuah tidak terdefinisi HTTP kode status alih-alih appropriate kode status.” Jump untuk quote
Google — meta tags melalui JavaScript, dan stateless rendering
«- “You can use JavaScript to set or change the meta description as well as the <title> element.”
Jump to quote
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus delapan, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
«- “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.”
Jump to quote
» (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus delapan, bagian kecil dua: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
«Google — deprecating hash-bang crawling (2015) » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus sembilan, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
«- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” Jump to 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.” Jump to quote » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus sepuluh, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Google — dynamic rendering adalah sebuah workaround (relayed; wording matches apa sudah cited di ini situs’s lebih luas JavaScript SEO guide, tidak independently re-verified ini lulus)
«- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” Jump to quote » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian seratus dua belas, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)
Bing — bingbot dan JavaScript
- “bingbot adalah generally able untuk render JavaScript.” (terjemahan) “bingbot adalah umumnya able untuk render JavaScript.” — bingbot Series, Bing Webmaster Blog. Baca post
- “bingbot melakukan tidak niscaya dukung all itu sama JavaScript kerangka kerja itu adalah supported di itu latest version dari Anda favorite modern browser.” (terjemahan) “bingbot melakukan tidak niscaya mendukung semua yang sama JavaScript kerangka kerja itu adalah didukung di latest versi dari Anda favorite modern browser.” — sama post. Baca post
Gary Illyes, Google — render timeouts buat duplicates (relayed melalui sebuah LinkedIn post, yang resists automated verification; treat sebagai tinggi-confidence sekunder)
- “itu centerpiece took forever untuk load, so rendering timed out… dan we adalah left dengan a bunch dari halaman itu hanya memiliki itu boilerplate. Dengan hanya itu boilerplate, itu halaman adalah 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.” Baca post
Yang 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 mendapatkan nyata HTML untuk ini route tanpa executing my JS?” — dan mulai oleh confirming route sebenarnya perlu untuk menjadi terindeks dan memeriksa apa sebuah direct, JS-free permintaan sudah mengembalikan; tidak setiap route perlu SSR, dan beberapa sudah kembalikan usable HTML.
1. adalah Anda starting sebuah baru bangun?
- Ya → gunakan meta-kerangka kerja (Berikutnya.js, Nuxt, Angular dengan SSR, SvelteKit, Remix). SSR/SSG dan per-route metadata come dibangun di. Berhenti di sini.
- Tidak → Continue.
2. melakukan Anda konten perubahan per pengguna / per permintaan?
- Tidak (mostly static konten) → Prerender / SSG di bangun time. Simplest robust perbaiki — setiap route adalah nyata HTML pada disk.
- Ya → rendering sisi server (SSR) so setiap permintaan mengembalikan route-spesifik HTML.
3. dapat’t melakukan SSR atau prerendering saat ini (legacy hand-rolled SPA)?
- gunakan dynamic rendering sebagai sebuah temporary bridge — sajikan crawler sebuah dirender snapshot. Plan migration untuk SSR/SSG; jangan treat ini sebagai destination.
4. Whatever path Anda chose, konfirmasi semua dari ini per route:
- nyata URL melalui History API (Tidak
#!fragments). - Unique judul + deskripsi meta + canonical di dirender DOM.
- Tidak ada apa pun lebih restrictive (
noindex, wrong canonical) baked ke mentah shell. - nyata
<a href>tautan antara routes. - Error routes mengembalikan nyata error status atau sebuah dirender
noindex(Tidak soft 404s). - setiap route di sitemap independently resolves untuk nyata HTML.
SPA SEO checklist
Addressability
- setiap view memiliki sebuah nyata URL melalui History API (
/products), tidak sebuah fragment (/#/products). - Inter-route tautan adalah nyata
<a href>anchors, tidak<div onClick>handlers.
konten availability
- setiap route mengembalikan nyata, unique HTML tanpa crawler executing Anda JS (SSR, prerendering/SSG, atau sebuah meta-kerangka kerja).
- Route konten memuat sebelum render window closes (hindari boilerplate-hanya duplicates).
Per-route indexability
- Unique
<title>dan deskripsi meta per route, present di dirender DOM. - Unique
rel=canonicalper route di dirender DOM. - Tidak stray
noindex/placeholder canonical di mentah shell itu sebuah lebih restrictive sinyal dapat lock di.
Errors
- “Tidak ditemukan” (terjemahan) “tidak ditemukan” routes redirect untuk sebuah URL milik siapa server mengembalikan sebuah nyata error status atau
carry
noindexditambahkan oleh JavaScript (Tidak soft 404s di200). - jika menggunakan
noindexroute, konfirmasi ini adalah ditambahkan setelah app decides route adalah tidak valid, tidak present dari very pertama paint — sebuah awalnoindexdapat penyebab Google untuk skip rendering halaman.
Sitemap
- setiap listed route independently resolves untuk nyata HTML.
- Dynamic routes (
/product/:id) enumerated dari app’s data sumber, tidak hand-maintained.
Verification
- Mentah HTML diperiksa di seluruh multiple routes (curl), confirming unique konten per URL.
- pemeriksaan URL mengonfirmasi dirender konten, judul, deskripsi, dan canonical per route.
mental models
1. dua halves Anda harus tidak conflate. Addressability (History API, unique URLs, Tidak fragments) dan konten availability (SSR, prerendering, atau sebuah meta-kerangka kerja) adalah independent. Bersih URLs dengan Tidak per-route HTML = sebuah tidy sitemap dari blank shells. Anda perlu keduanya.
2. “Apa akan itu server return, fresh?” (terjemahan) “Apa akan server kembalikan, fresh?”
seluruh SPA masalah adalah itu browser view diverges dari server respons. untuk
apa pun route, tanyakan apa sebuah curl dengan Tidak JS execution mendapatkan 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 dengan sengaja dibuat ini mengembalikan nyata error status atau sebuah dirender noindex.
4. paling-restrictive-directive trap.
Google reconciles mentah vs. dirender oleh taking lebih restrictive sinyal. sebuah noindex
atau wrong canonical di shell dapat override Anda JS “perbaiki.” (terjemahan) “perbaiki.” Audit mentah HTML, tidak hanya
dirender DOM.
5. Meta-kerangka kerja pertama untuk baru membangun. Hand-rolling client-side routing berarti owning setiap satu dari ini masalah. Picking sebuah kerangka kerja itu melakukan SSR/SSG dan per-route metadata adalah choosing tidak untuk memiliki them.
SPA SEO cheat sheet
Routing
| Approach | URL contoh | Reaches server? | Google-safe? |
|---|---|---|---|
| History API | /products | Ya | Ya (recommended) |
| Hash routing | /#/products | Tidak (fragment tidak pernah dikirim) | Tidak — deprecated 2015 |
Hash-bang (#!) | /#!/products | Tidak | Tidak — legacy AJAX scheme, deprecated |
rendering strategies
| Strategy | crawler mendapatkan nyata HTML tanpa berjalan JS? | Best untuk |
|---|---|---|
| SSR | Ya | Per-pengguna / per-permintaan konten |
| Prerendering / SSG | Ya | Mostly-static konten |
| Meta-kerangka kerja (Berikutnya/Nuxt/dan seterusnya.) | Ya (dibangun di) | baru membangun |
| Dynamic rendering | Ya, untuk bot hanya | Temporary bridge pada legacy SPAs |
| Bare CSR (client-hanya) | Tidak | Tidak ada apa pun Anda ingin terindeks reliably |
Per-route harus-haves (semua di dirender 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 mengapa hash routing fails. - Client-side routers pertahankan
200untuk semuanya, including “tidak ditemukan” (terjemahan) “tidak ditemukan” → soft 404s. - Google reconciles mentah vs. dirender oleh taking lebih restrictive sinyal.
- Tidak special SPA sitemap format ada — setiap listed route harus resolve untuk nyata HTML.
SPA SEO anti-patterns
Shipping sebuah bare CSR SPA dan submitting sebuah penuh sitemap. sitemap lists 500 routes; semua 500 mengembalikan sama shell untuk sebuah fresh fetch. sitemap tidak membuat konten exist — rendering melakukan.
menambahkan History API dan calling ini selesai. Bersih URLs perbaiki addressability, tidak konten. Tanpa 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; tidak 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 mentah shell “that JS will overwrite.” (terjemahan) “itu JS akan overwrite.”
Google takes lebih restrictive dari mentah vs. dirender. shell’s directive dapat win dan
silently suppress route.
Returning 200 untuk “tidak ditemukan” (terjemahan) “tidak ditemukan” views.
Soft 404s mendapatkan thin/empty halaman terindeks. Redirect untuk sebuah nyata error status atau tambahkan sebuah
dirender 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 ada di client state tidak akan menjadi di sana ketika Google renders berikutnya route.
umum SPA pengindeksan issues
setiap route mengembalikan yang sama HTML
Symptom: /products dan /about memiliki berbeda browser views tetapi identical mentah
respons. mungkin penyebab: History API routing menyediakan addressability tanpa SSR
atau prerendering. Perbaiki: Produce route-spesifik HTML dan konfirmasi sebuah direct permintaan untuk
setiap path berisi -nya own heading, body copy, dan metadata.
Missing routes muncul sebagai successful halaman
Symptom: sebuah nonexistent route menampilkan sebuah tidak-ditemukan view sementara returning 200.
mungkin penyebab: client router owns error setelah server memiliki sudah dikirim sebuah
successful shell. Perbaiki: mengembalikan right server status; jika itu cannot ship namun,
redirect untuk sebuah URL dengan sebuah nyata error status atau render noindex. Konfirmasi dengan sebuah fresh
direct permintaan, tidak sebuah di-app navigation.
Routes collapse ke duplicates setelah rendering
Symptom: mesin pencari cluster distinct URLs atau retain hanya shared navigation. mungkin penyebab: Route konten memuat too late dan rendering captures boilerplate. Perbaiki: Prioritize utama konten di server respons atau earliest render path. Konfirmasi several routes expose unique konten sebelum opsional scripts finish.
Testing apa server sebenarnya mengembalikan per route
SPA trap adalah itu routes differ di browser tetapi tidak pada wire. periksa multiple routes di mentah-HTML tingkat, tidak satu.
Fetch mentah 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 dua routes’ mentah 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 mentah 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 “tidak ditemukan” (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 dirender 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 tidak 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. dirender HTML untuk sebuah route dan konfirmasi per-route judul, deskripsi, dan canonical sebenarnya land.
- Rich hasil Test / pemeriksaan URL “View crawled halaman” (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 bandingkan mentah HTML dari several routes dan catch sebuah shared shell.- Screaming Frog SEO Spider (JavaScript rendering mode) — crawl situs dengan dan tanpa rendering untuk bandingkan mentah vs. dirender 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 karena slow routes dapat time out ke boilerplate duplicates.
- Bing Webmaster alat — pemeriksaan URL — bingbot renders JS lebih sedikit consistently daripada Googlebot, so konfirmasi Anda routes pada Bing’s side too.
Prove sebuah SPA route adalah independently dapat diindeks
Test direct-entry HTML parity
Test untuk jalankan: Open representative routes di sebuah baru session dan fetch yang sama URLs
dengan curl. Yang diharapkan hasil: setiap URL mengembalikan -nya own utama konten dan head
sinyal tanpa prior app state. Failure interpretation: route bergantung pada
client navigation atau stored state. Monitoring window: Immediate. Rollback
trigger: sebuah route berfungsi hanya setelah entering melalui homepage.
Test error status menangani
Test untuk jalankan: permintaan sebuah known-tidak valid route secara langsung dan inspect keduanya status dan
dirender directives. Yang diharapkan hasil: sebuah genuine error status, atau terdokumentasi
fallback dari sebuah dirender noindex/redirect untuk sebuah error respons. Failure
interpretation: SPA adalah generating soft 404s. Monitoring window: Immediate.
Rollback trigger: Tidak valid paths ship sebagai dapat diindeks 200 halaman.
Test metadata isolation
Test untuk jalankan: Bandingkan mentah dan dirender judul, robots tag, dan canonical di seluruh setidaknya three routes. Yang diharapkan hasil: setiap route memiliki satu yang dimaksud, 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.
Uji pemahaman Anda: 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 penuh JS-SEO guide, including “jangan gunakan fragments di URLs” (terjemahan) “jangan gunakan fragments di URLs” dan app-shell/duplicate-konten bagian itu ini artikel membangun pada.
- Beginner’s Guide untuk SEO teknis — di mana rendering dan crawling fit di bigger picture.
My speaking
- Bagaimana Penelusuran berfungsi (SlideShare) — my walkthrough dari crawling, rendering, pengindeksan, dan peringkat, yang adalah pipeline sebuah SPA memiliki untuk survive. (My standing disclaimer applies: “Ini adalah my understanding dari sistem… tidak going untuk menjadi 100% menyelesaikan atau accurate.” (terjemahan) “ini adalah my understanding dari sistem… tidak going untuk menjadi 100% menyelesaikan atau accurate.”)
dari sekitar industry
- Google — memahami JavaScript SEO basics — app-shell model dan “Gunakan itu History API alih-alih dari fragments.” (terjemahan) “gunakan History API alih-alih fragments.”
- Google — Perbaiki Penelusuran-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 Tidak 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 tidak menjadi cloaking. (SER’s paraphrase dari Illyes, dated 2015 — sekunder.)
- SEO untuk Single halaman Applications (Nuxt SEO) — sebuah kerangka kerja-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 di sini. 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.