Optimasi SEO pada React Router v7 bisa kita lakukan dengan beberapa pendekatan. Selain tentunya konten yang harus berkualitas dan unik, kita bisa memaksimalkan Search Engine Optimization dari segi kodingan. Dan kali ini saya akan coba berbagi pengalaman saya dalam melakukannya di React Router x7.
Optimasi SEO sering dianggap sulit ketika membangun aplikasi berbasis React, apalagi untuk aplikasi SPA (Single Page Application), bahkan ketika menggunakan React Router v7 mode framework yang notabene sudah support SSR, banyak developer khawatir soal crawling, indexing, hingga duplikasi URL.
Kabar baiknya, React Router v7 sudah sangat SEO-friendly jika dioptimasi dengan benar. Artikel ini membahas langkah konkret yang pernah saya implementasikan, plus rekomendasi lanjutan agar website dengan React Router mode framework lebih optimal di mesin pencari seperti Google.
Kenapa SEO Penting di Aplikasi Web?
Sebelumnya, mari kita bahas sedikit tentang alasan kenapa SEO itu penting utuk aplikasi web kita. Tanpa menerapkan optimasi SEO yang tepat, aplikasi web kita berisiko mengalami hal-hal seperti di bawah ini:
- Halaman tidak terindeks Google
- Konten dinamis tidak terbaca crawler
- Duplikasi URL karena parameter filter
- Ranking kalah dari website statis atau CMS
React Router v7 memberikan kontrol penuh terhadap routing, meta tag, dan server-side handling, sehingga SEO bisa dioptimasi secara maksimal.
Pada proyek yang pernah saya kerjakan, ada beberapa pendekatan yang saya lakukan untuk optimasi SEO di proyek React Router v7.
- Implementasi robots.txt
- Implementasi sitemap.xml
- Menambahkan canonical url
- Implementasi structured data (json-ld)
- Verifikasi meta tag
Mari kita bahas satu per satu!
Implementasi robots.txt
Langkah pertama adalah memastikan crawler mesin pencari tahu bagian mana dari website yang boleh dan tidak boleh diakses. Fungsi utama robots.txt adalah memberi instruksi kepada web crawler (robot mesin pencari) bagian mana dari situs web yang boleh dan tidak boleh diakses untuk dirayapi (crawl) dan diindeks, membantu mengontrol lalu lintas server, mencegah duplicate content, dan menjaga hasil pencarian tetap relevan, meskipun tidak berfungsi sebagai pengaman keamanan karena file ini publik.
File robots.txt bukan fitur keamanan. File ini bersifat publik dan dapat dibaca siapa saja, termasuk bot jahat. Jangan gunakan untuk menyembunyikan informasi sensitif.
Gunakan perintah Disallow untuk melarang perayapan dan Allow (jarang dipakai) untuk mengizinkan, serta Sitemap untuk menunjuk lokasi sitemap kita.
Aturan robots.txt bersifat instruktif; bot yang tidak patuh (seperti bot spam) mungkin tetap akan mengabaikannya.
Untuk menambahkan file robots.txt di aplikasi React Router v7 mode framework, kita bisa membuat file dengan nama robots[.]txt.ts di root dari proyek kita.
1import type { Route } from './+types/robots[.]txt'
2
3export const loader = ({}: Route.LoaderArgs) => {
4 const robotText = `
5User-agent: *
6Allow: /
7Disallow: /private/
8Disallow: /admin/
9
10Sitemap: https://yourdomain.com/sitemap.xml
11`
12
13 return new Response(robotText, {
14 status: 200,
15 headers: {
16 'Content-Type': 'text/plain',
17 },
18 })
19}
1import type { Route } from './+types/robots[.]txt'
2
3export const loader = ({}: Route.LoaderArgs) => {
4 const robotText = `
5User-agent: *
6Allow: /
7Disallow: /private/
8Disallow: /admin/
9
10Sitemap: https://yourdomain.com/sitemap.xml
11`
12
13 return new Response(robotText, {
14 status: 200,
15 headers: {
16 'Content-Type': 'text/plain',
17 },
18 })
19}
Text sitemap.xml pada kode di atas adalah path atau url file sitemap.xml kita. Bagaimana cara membuat file sitemap.xml ini? Mari kita lanjutkan!
Implementasi sitemap.xml
Karena file robots.txt ada dependensi ke file sitemap.xml, maka kita perlu untuk membuat file sitemap.xml ini. Sitemap adalah fondasi SEO modern. Tanpa sitemap, Google akan lebih lambat menemukan dan mengindeks halaman baru.
File sitemap.xml berfungsi sebagai peta teknis berisi daftar seluruh URL penting website untuk membantu mesin pencari (seperti Google) menemukan, merayapi (crawl), dan mengindeks konten lebih cepat dan efisien. Ini mempermudah pemahaman struktur situs, pembaruan konten, dan meningkatkan visibilitas SEO.
Untuk menambahkan file sitemap.xml di aplikasi React Router v7 mode framework, kita bisa membuat file dengan nama sitemap[.]xml.ts di root dari proyek kita.
1import { createHash } from 'node:crypto'
2import { getBlog } from '#/database/services/blog.service'
3import { getUsers } from '#/database/services/users.service'
4import type { Route } from './+types/sitemap[.]xml'
5
6export const loader = async ({ request }: Route.LoaderArgs) => {
7 const baseUrl = 'https://yourdomain.com'
8
9 const staticRoutes = [
10 { path: '/', priority: 1.0, changefreq: 'daily' },
11 { path: '/blog', priority: 0.9, changefreq: 'daily' },
12 { path: '/users', priority: 0.9, changefreq: 'daily' },
13 { path: '/about-us', priority: 0.8, changefreq: 'monthly' },
14 ]
15
16 // Dynamic routes from blog post and users
17 const [blogs, users] = await Promise.all([getBlog(), getUsers()])
18
19 const toLastMod = (v: unknown) => {
20 const d1 = new Date(Number(v))
21 if (!Number.isNaN(d1.getTime())) return d1.toISOString()
22 const d2 = new Date(v as any)
23 if (!Number.isNaN(d2.getTime())) return d2.toISOString()
24 return undefined
25 }
26
27 const url = (
28 loc: string,
29 opts?: { lastmod?: string; changefreq?: string; priority?: number }
30 ) => {
31 return `
32 <url>
33 <loc>${loc}</loc>${opts?.lastmod ? `n <lastmod>${opts.lastmod}</lastmod>` : ''}${opts?.changefreq ? `n <changefreq>${opts.changefreq}</changefreq>` : ''}${opts?.priority != null ? `n <priority>${opts.priority}</priority>` : ''}
34 </url>`
35 }
36
37 const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
38<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
39 ${staticRoutes.map((route) => url(`${baseUrl}${route.path}`, { changefreq: route.changefreq, priority: route.priority })).join('')}
40
41 ${blogs
42 .map((blog) =>
43 url(`${baseUrl}/blog/${blog.slug}`, {
44 lastmod: toLastMod(blog.updated_at),
45 changefreq: 'weekly',
46 priority: 0.8,
47 })
48 )
49 .join('')}
50 ${users
51 .map((user) =>
52 url(`${baseUrl}/user/${user.slug}`, {
53 lastmod: toLastMod(user.updated_at),
54 changefreq: 'weekly',
55 priority: 0.8,
56 })
57 )
58 .join('')}
59</urlset>
60`
61
62 const etag = `"${createHash('sha1').update(sitemap).digest('hex')}"`
63 const incomingEtag = request.headers.get('If-None-Match')
64
65 if (incomingEtag && incomingEtag === etag) {
66 return new Response(null, {
67 status: 304,
68 headers: {
69 ETag: etag,
70 'Content-Type': 'application/xml',
71 'Cache-Control': 'public, max-age=3600',
72 },
73 })
74 }
75
76 return new Response(sitemap, {
77 status: 200,
78 headers: {
79 ETag: etag,
80 'Content-Type': 'application/xml',
81 'Cache-Control': 'public, max-age=3600',
82 },
83 })
84}
85
1import { createHash } from 'node:crypto'
2import { getBlog } from '#/database/services/blog.service'
3import { getUsers } from '#/database/services/users.service'
4import type { Route } from './+types/sitemap[.]xml'
5
6export const loader = async ({ request }: Route.LoaderArgs) => {
7 const baseUrl = 'https://yourdomain.com'
8
9 const staticRoutes = [
10 { path: '/', priority: 1.0, changefreq: 'daily' },
11 { path: '/blog', priority: 0.9, changefreq: 'daily' },
12 { path: '/users', priority: 0.9, changefreq: 'daily' },
13 { path: '/about-us', priority: 0.8, changefreq: 'monthly' },
14 ]
15
16 // Dynamic routes from blog post and users
17 const [blogs, users] = await Promise.all([getBlog(), getUsers()])
18
19 const toLastMod = (v: unknown) => {
20 const d1 = new Date(Number(v))
21 if (!Number.isNaN(d1.getTime())) return d1.toISOString()
22 const d2 = new Date(v as any)
23 if (!Number.isNaN(d2.getTime())) return d2.toISOString()
24 return undefined
25 }
26
27 const url = (
28 loc: string,
29 opts?: { lastmod?: string; changefreq?: string; priority?: number }
30 ) => {
31 return `
32 <url>
33 <loc>${loc}</loc>${opts?.lastmod ? `n <lastmod>${opts.lastmod}</lastmod>` : ''}${opts?.changefreq ? `n <changefreq>${opts.changefreq}</changefreq>` : ''}${opts?.priority != null ? `n <priority>${opts.priority}</priority>` : ''}
34 </url>`
35 }
36
37 const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
38<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
39 ${staticRoutes.map((route) => url(`${baseUrl}${route.path}`, { changefreq: route.changefreq, priority: route.priority })).join('')}
40
41 ${blogs
42 .map((blog) =>
43 url(`${baseUrl}/blog/${blog.slug}`, {
44 lastmod: toLastMod(blog.updated_at),
45 changefreq: 'weekly',
46 priority: 0.8,
47 })
48 )
49 .join('')}
50 ${users
51 .map((user) =>
52 url(`${baseUrl}/user/${user.slug}`, {
53 lastmod: toLastMod(user.updated_at),
54 changefreq: 'weekly',
55 priority: 0.8,
56 })
57 )
58 .join('')}
59</urlset>
60`
61
62 const etag = `"${createHash('sha1').update(sitemap).digest('hex')}"`
63 const incomingEtag = request.headers.get('If-None-Match')
64
65 if (incomingEtag && incomingEtag === etag) {
66 return new Response(null, {
67 status: 304,
68 headers: {
69 ETag: etag,
70 'Content-Type': 'application/xml',
71 'Cache-Control': 'public, max-age=3600',
72 },
73 })
74 }
75
76 return new Response(sitemap, {
77 status: 200,
78 headers: {
79 ETag: etag,
80 'Content-Type': 'application/xml',
81 'Cache-Control': 'public, max-age=3600',
82 },
83 })
84}
85
Kode di atas adalah contoh sitemap yang menghasilkan list url statis dan juga dinamis (dari data blog post dan juga user).
Menambahkan Canonical URL
Langkah optimasi SEO react router framework mode selanjutnya adalah dengan menambahkan canonical url. Canonical URL (URL kanonis) adalah tag HTML (rel="canonical") yang memberi tahu mesin pencari (seperti Google) bahwa URL tertentu adalah versi utama atau "master" dari suatu konten, terutama jika terdapat konten duplikat atau sangat mirip di beberapa URL berbeda. Contohnya:
1/windows
2/windows?category=finance
3/windows?sort=popular
4
1/windows
2/windows?category=finance
3/windows?sort=popular
4
Tanpa canonical, Google menganggap ini sebagai halaman berbeda. Canonical url ini perlu ditambahkan di setiap halaman yang ingin dioptimasi. Berikut adalah contoh implementasi canonical url untuk halaman About Us
1export function meta({ location }: { location: { pathname: string } }) {
2 const canonicalUrl = new URL(location.pathname, siteConfig.baseUrl).toString()
3 return [
4 { title: `About Us | Your Website Name` },
5 { tagName: 'link', rel: 'canonical', href: canonicalUrl },
6 ]
7}
8
1export function meta({ location }: { location: { pathname: string } }) {
2 const canonicalUrl = new URL(location.pathname, siteConfig.baseUrl).toString()
3 return [
4 { title: `About Us | Your Website Name` },
5 { tagName: 'link', rel: 'canonical', href: canonicalUrl },
6 ]
7}
8
Implementasi Structured Data (JSON-LD)
Selanjutnya, kita akan mengimplementasikan Structured Data (JSON-LD). Structured Data JSON-LD (JavaScript Object Notation for Linking Data) adalah format kode standar untuk menyusun data situs web agar mudah dipahami mesin pencari seperti Google.
Untuk menambahkan JSON-LD di React Router v7, kita perlu menambahkan kode berikut di dalam function meta di setiap halaman. Jangan lupa untuk menyesuaikan datanya.
1export function meta({ location }: { location: { pathname: string } }) {
2 const canonicalUrl = new URL(location.pathname, siteConfig.baseUrl).toString()
3
4 const structuredData = {
5 '@context': 'https://schema.org',
6 '@type': 'AboutPage',
7 name: `About Us | ${siteConfig.title}`,
8 description:
9 'Bringing Together Leading Organizations to Enhance Capacity Building on Sustainable Finance.',
10 url: canonicalUrl,
11 publisher: {
12 '@type': 'Organization',
13 name: 'Your Website Title',
14 url: 'https://yourdomain.com',
15 },
16 }
17
18 return [
19 { title: `About Us | ${siteConfig.title}` },
20 { tagName: 'link', rel: 'canonical', href: canonicalUrl },
21 {
22 'script:ld+json': structuredData,
23 },
24 ]
25}
26
1export function meta({ location }: { location: { pathname: string } }) {
2 const canonicalUrl = new URL(location.pathname, siteConfig.baseUrl).toString()
3
4 const structuredData = {
5 '@context': 'https://schema.org',
6 '@type': 'AboutPage',
7 name: `About Us | ${siteConfig.title}`,
8 description:
9 'Bringing Together Leading Organizations to Enhance Capacity Building on Sustainable Finance.',
10 url: canonicalUrl,
11 publisher: {
12 '@type': 'Organization',
13 name: 'Your Website Title',
14 url: 'https://yourdomain.com',
15 },
16 }
17
18 return [
19 { title: `About Us | ${siteConfig.title}` },
20 { tagName: 'link', rel: 'canonical', href: canonicalUrl },
21 {
22 'script:ld+json': structuredData,
23 },
24 ]
25}
26
Dengan begitu, seharunya script JSON-LD sudah termuat di setiap halaman. Untuk mengeceknya kita bisa inspect element di browser, lalu cari dengan keyword <script type="application/ld+json">. Jika kode tesebut ada, berarti kita telah berhasil memasang JSON-LD.