IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA32/32-dars20 daqiqa
Mundarija (31)

SPA uchun SEO: React ilovani Google qanday ko'radi, canonical, hreflang, sitemap va prerender

Qisqacha: SPA sahifasi serverdan deyarli bo'sh HTML bo'lib keladi — mazmunni JS chizadi. Google JS'ni bajaradi (keyinroq, navbat bilan), lekin Telegram'ning havola ko'rinishi va ko'p botlar bajarmaydi. Shuning uchun: har sahifaga <title> va description (React 19 ularni <head> ga o'zi ko'taradi), dublikatlarga canonical, tillarga hreflang, mavjud bo'lmagan manzilga noindex, havola ko'rinishi uchun statik Open Graph va sitemap.xml. Ommaviy sahifalar ko'p bo'lsa — prerender yoki Next.js/Astro. vazifalar v11: canonical + 5 ta hreflang, 404 — noindex, statik og:*; prerender o'lchanmadi (ilova kirish ortida).

Bu darsda

  • CSR sahifani Googlebot va JS bajarmaydigan botlar qanday ko'rishini tushuntirasiz.
  • React 19 bilan har sahifaga title, meta va link qo'yasiz va natijani <head> da tekshirasiz.
  • Canonical va hreflang'ni to'g'ri hisoblaydigan komponent yozasiz; soft 404 ni noindex bilan yopasiz.
  • sitemap.xml ni marshrutlardan yaratasiz va Open Graph nega statik bo'lishi kerakligini bilasiz.
  • Prerender, SSR va SSG orasida qaror qilasiz: SPA qachon yetadi, qachon Next.js yoki Astro.

Oldin bilishingiz kerak: Qidiruv tizimlari uchun meta teglar, Open Graph, SEO uchun HTML amaliyoti, Hujjat metadata va resurs API'lari, i18n II: til URL'da, Yuklanishni optimallashtirish.

1. Nega bu kerak?

Jasur aka «Bahor» uchun ikki sayt qildirmoqchi. Birinchisi — menyu va manzil: odamlar Google'da "Chilonzor osh" deb qidirib topishi kerak. Ikkinchisi — xodimlar uchun ichki ilova (vazifalar kabi): unga kirish paroli bilan, Google'da chiqishi umuman kerak emas.

Ikkalasi ham React bilan yozilishi mumkin. Lekin SEO talablari butunlay boshqa. Bu dars ikki savolga javob beradi: React SPA'ni qidiruv tizimiga qanday ko'rsatish, va qachon SPA yetmaydi — server tomonda chizish (SSR) yoki oldindan chizish (SSG) kerak bo'ladi.

SEO uchun HTML qismida statik sahifalar uchun qoidalarni ko'rgan edik: sarlavha, tavsif, semantik HTML. SPA'da bitta muammo qo'shiladi: HTML'ning o'zida mazmun yo'q.

Hayotiy o'xshatish: do'kon vitrinasi. Oddiy sayt — tovarlari vitrinada turgan do'kon: o'tkinchi darhol ko'radi. SPA — vitrinasi bo'sh, ichkariga kirganingizda sotuvchi tovarlarni chiqarib qo'yadigan do'kon. Sabrli xaridor (Google) kiradi va ko'radi. Shoshgan o'tkinchi (Telegram havola ko'rinishi) faqat bo'sh vitrinani ko'rib o'tib ketadi.

2. Qidiruv tizimi SPA'ni qanday ko'radi

2.1 Bo'sh vitrina: JS'siz HTML

vazifalar ning index.html i — serverdan keladigan yagona HTML. JS bajarmaydigan bot nimani ko'radi? Uni bot kabi o'qiymiz — faqat matn, regex bilan (HTML qisqartirilgan, CSP va izohlar olib tashlangan):

js
const html = `<!DOCTYPE html>
<html lang="uz">
<head>
  <title>Vazifalar — kichik vazifalar ro'yxati</title>
  <meta name="description"
        content="React'da yozilgan vazifalar ro'yxati.">
  <meta property="og:type" content="website">
  <meta property="og:title" content="Vazifalar">
  <meta property="og:locale" content="uz_UZ">
  <script type="module" src="/assets/index-CKFkeGF8.js"></script>
</head>
<body>
  <main>
    <h1>Vazifalar</h1>
    <div id="ilova"></div>
  </main>
</body>
</html>`;

const ol = (re) => html.match(re)?.[1] ?? "(yo'q)";
console.log("title:", ol(/<title>(.*?)<\/title>/));
console.log("og:title:", ol(/property="og:title" content="(.*?)"/));
console.log("canonical:", ol(/rel="canonical" href="(.*?)"/));
const matn = html
  .replace(/<head>[\s\S]*<\/head>/, "")
  .replace(/<[^>]+>/g, " ")
  .split(/\s+/)
  .filter(Boolean);
console.log("sahifa matni:", matn.join(" "));

Konsolda:

text
title: Vazifalar — kichik vazifalar ro'yxati
og:title: Vazifalar
canonical: (yo'q)
sahifa matni: Vazifalar

Bot uchun sahifada bitta so'z: «Vazifalar». Ro'yxat, statistika, sozlamalar — hammasi <div id="ilova"> ichida, JS chizgandan keyin. Sarlavha va og:* esa bor — chunki ular HTML'ga qo'lda yozilgan. Canonical yo'q — u JS bilan qo'shiladi (keyinroq ko'ramiz).

2.2 Googlebot: ikki to'lqin

Google'ning hujjatiga ko'ra ("JavaScript SEO basics"), JS sahifa uch bosqichdan o'tadi:

flowchart LR
  A["Crawl: HTML yuklandi"] --> B["Havolalar va meta — darhol"]
  A --> C["Render navbati"]
  C --> D["Headless Chromium: JS bajarildi"]
  D --> E["Chizilgan HTML — indeks"]
  E --> F["Yangi havolalar — crawl'ga"]

Diagrammaga qarang: birinchi to'lqinda Google faqat xom HTML'ni ko'radi. JS bajarilishi uchun sahifa navbatga tushadi — Chromium'ning yangi versiyasi bilan chiziladi. Navbat soniyalar ham, uzoqroq ham bo'lishi mumkin. Natija: Google SPA'ni ko'radi, lekin sekinroq va ishonchliligi kamroq.

Boshqa o'quvchilar esa JS bajarmaydi yoki cheklangan bajaradi:

Kim JS bajaradimi Nimani ko'radi
Googlebot ha (navbat bilan) chizilgan sahifa
Telegram, WhatsApp, Facebook havola ko'rinishi yo'q faqat HTML'dagi og:* va <title>
AI qidiruvlari, boshqa botlar ko'pincha yo'q xom HTML

Shuning uchun asosiy qoida: havola ko'rinishi uchun kerak narsa HTML'da statik bo'lsin. vazifalar og:* teglarini aynan shuning uchun index.html ga qo'ydi (kanon izohi):

html
<!-- 21/#32: havola ko'rinishi (Telegram, WhatsApp, Facebook) JS'ni
     bajarmaydi — faqat shu statik teglar o'qiladi -->
<meta property="og:type" content="website">
<meta property="og:title" content="Vazifalar">
<meta property="og:description"
      content="Kichik vazifalar ro'yxati: oflayn ishlaydi, o'zbek, rus va ingliz tilida.">
<meta property="og:locale" content="uz_UZ">

Cheklov — TEXNIK-QARZ 69: og:* hamma sahifa uchun bir xil, og:image yo'q (absolyut manzil kerak). SPA'da har sahifaga o'z og:* — faqat prerender yoki server bilan.

Tekshirib ko'ring: Malika Telegram'da do'stiga vazifalar dagi /statistika havolasini yubordi. Havola ko'rinishida nima chiqadi — "Statistika" sarlavhasimi yoki boshqa narsa?

Javob

«Vazifalar» va «Kichik vazifalar ro'yxati: …» — index.html dagi statik og:* teglar. Telegram JS bajarmaydi, shuning uchun StatistikaSahifasi dagi <title>Statistika — Vazifalar</title> ni ko'rmaydi. Server har manzilga bir xil index.html beradi. Sahifaga xos ko'rinish uchun server har manzilga o'z HTML'ini berishi kerak — prerender yoki SSR.

3.1 Avtomatik ko'tarish

Hujjat metadata darsida ko'rgandik: React 19 da komponent ichida <title>, <meta> va <link> yozsangiz, React ularni <head> ga ko'taradi (hoist). Komponent yo'qolsa — olib tashlaydi. vazifalar da har sahifa 18-qismdan beri o'z sarlavhasini shunday beradi.

Jonli: ikki sahifa, har birida o'z teglari. Statistikaga o'tamiz va <head> ni o'qiymiz:

jsx
import { useEffect } from "react";
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useLocation,
} from "react-router";

const SAYT = "https://example.com";

function Seo({ sarlavha, tavsif }) {
  const { pathname } = useLocation();
  return (
    <>
      <title>{`${sarlavha} — Bahor`}</title>
      <meta name="description" content={tavsif} />
      <link rel="canonical" href={SAYT + pathname} />
    </>
  );
}

function Qobiq() {
  const { pathname } = useLocation();
  useEffect(() => {
    const teglar = document.head.querySelectorAll(
      "title, meta[name=description], link[rel=canonical]",
    );
    console.log(pathname, teglar.length, document.title);
  }, [pathname]);
  return (
    <>
      <Link to="/">Menyu</Link>{" "}
      <Link to="/statistika">Statistika</Link>
      <Outlet />
    </>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Qobiq />,
    children: [
      {
        index: true,
        element: <Seo sarlavha="Menyu" tavsif="Osh va manti" />,
      },
      {
        path: "statistika",
        element: <Seo sarlavha="Statistika" tavsif="Sotuv" />,
      },
    ],
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Konsolda:

text
/ 3 Menyu — Bahor
/statistika 3 Statistika — Bahor

Har sahifada <head> da aynan 3 ta teg — eski sahifaniki olib tashlandi, yangisi qo'yildi. Takror yo'q. (Natija oynasida manzil satri yo'q, shuning uchun xotiradagi router; loyihada — createBrowserRouter.)

Bitta tafsilot: <title> ichida bitta satr bo'lishi kerak. <title>{sarlavha} — Bahor</title> deb yozsangiz, JSX uni ikki bola (satr + satr) qiladi va React dev rejimida ogohlantiradi. Shuning uchun shablon satri: {`${sarlavha} — Bahor`}.

3.2 react-helmet-async — eski kodda

React 19 gacha bu ish uchun react-helmet-async kutubxonasi ishlatilardi: <Helmet><title>…</title></Helmet>, ilova esa <HelmetProvider> ichida. Ko'p mavjud loyihalarda uchratasiz. React 19 da brauzerdagi SPA uchun u ortiqcha — yuqoridagi o'rnatilgan imkoniyat aynan shuni qiladi. Server tomonda chizishda (SSR) <head> ni yig'ish uchun Next.js'ning o'z metadata API'si bor (Metadata API).

4. Canonical: bitta sahifa — bitta manzil

4.1 Dublikatlar qayerdan keladi

Bitta ro'yxat sahifasi bir nechta manzilda ochiladi:

  • /vazifalar/ va /vazifalar/?filtr=faol va /vazifalar/?filtr=bajarilgan (URL — holat);
  • /vazifalar va /vazifalar/ (oxirgi chiziqcha);
  • reklama belgilari bilan: ?utm_source=telegram.

Qidiruv tizimi ularni turli sahifalar deb hisoblashi mumkin — "ball" bo'linadi va qaysi biri natijada chiqishi tasodifga qoladi. Canonical (<link rel="canonical" href="…">) — "bu sahifaning asl manzili shu" degan ko'rsatma (meta teglar). Qidiruv tizimi uni maslahat sifatida qabul qiladi.

4.2 Kanon qarori: so'rovsiz manzil

vazifalar da canonical — location.pathname, ya'ni so'rov qismisiz (?filtr=… — o'sha sahifaning holati, alohida sahifa emas). Har til o'z canonical'iga ega: /ru/statistika ning canonical'i — /ru/statistika, /statistika emas. Tillar dublikat emas — ular hreflang bilan bog'lanadi.

5. hreflang: bir sahifa — to'rt til

5.1 Nega kerak

i18n II darsida vazifalar tilni manzilga qo'ydi: /statistika (o'zbek lotin), /uz-cyrl/statistika, /ru/statistika, /en/statistika. Qidiruv tizimi bular bir sahifaning to'rt tili ekanini qayerdan biladi? hreflang orqali:

html
<link rel="alternate" hreflang="uz" href="https://…/statistika">
<link rel="alternate" hreflang="ru" href="https://…/ru/statistika">
<link rel="alternate" hreflang="x-default"
      href="https://…/statistika">

Uch qoida (Google hujjati bo'yicha):

  1. Har til hamma tillarni ko'rsatadi, o'zini ham. Ruscha sahifada ham to'rttala alternate turadi.
  2. O'zaro bo'lsin: uz sahifa ru ni ko'rsatsa, ru ham uz ni ko'rsatishi shart, aks holda e'tiborsiz qoladi.
  3. x-default — til mos kelmaganda (masalan, nemis tilidagi foydalanuvchiga) qaysi sahifa. vazifalar da — o'zbekcha.

Til kodi — BCP 47: uz, uz-Cyrl (yozuv bilan), ru, en. Manzildagi prefiks esa kichik harf — uz-cyrl. Bular bir xil narsa emas: biri til kodi, biri manzil bo'lagi.

5.2 Kanon: SeoTeglari.tsx

Kanon komponenti — kod aynan (fayl boshidagi izoh qisqartirildi):

tsx
import { useLocation } from "react-router";
import { TILLAR } from "../i18n/index.ts";
import { tildaYol } from "../i18n/yol.ts";

export function SeoTeglari() {
  // pathname — basename'siz; to'liq yo'l — brauzer manzilidan
  useLocation();
  const toliq = (til: (typeof TILLAR)[number]) =>
    new URL(
      tildaYol(location.pathname, "", til, import.meta.env.BASE_URL),
      location.origin,
    ).href;
  return (
    <>
      <link
        rel="canonical"
        href={new URL(location.pathname, location.origin).href}
      />
      {TILLAR.map((til) => (
        <link
          key={til}
          rel="alternate"
          hrefLang={til}
          href={toliq(til)}
        />
      ))}
      <link rel="alternate" hrefLang="x-default" href={toliq("uz")} />
    </>
  );
}

Uch nozik joy:

  • useLocation() — qiymati ishlatilmaydi. U faqat obuna uchun: manzil o'zgarganda komponent qayta chiziladi. To'liq yo'l esa window.location dan olinadi, chunki router pathname i basename'siz (/ru va ilova papkasi kesilgan). Bu — i18n II dagi basename qarorining natijasi.
  • tildaYol(…, "", til, …) — ikkinchi parametr bo'sh: so'rov qismi (?filtr=) hreflang'ga ham kirmaydi.
  • hrefLang — React'da camelCase. HTML'ga hreflang bo'lib tushadi.

Qobiq.tsx da <SeoTeglari /> bir marta — har sahifa uchun. React 19 uni <head> ga ko'taradi.

6. Soft 404 va noindex

SPA'da mavjud bo'lmagan manzil (/vazifalar/xatoli-sahifa) ham ko'pincha 200 javob oladi: server har manzilga index.html beradi (SPA zaxirasi), "Sahifa topilmadi" ni esa JS chizadi. Qidiruv tizimi uchun bu "soft 404": status — "bor", mazmun — "yo'q". Bunday sahifalar indeksga tushishi mumkin.

vazifalar ning ikki xostingi bu yerda farq qiladi (kanon izohlari): GitHub Pages 404.html ni haqiqiy 404 status bilan beradi, Vercel'ning rewrites i esa 200 bilan. Ikkalasida ham ishlaydigan yechim — 404 sahifada noindex (kanon):

tsx
<title>
  {t("sahifaNomi", { sahifa: t("topilmadi.sarlavha") })}
</title>
{/* 21/#32: SPA zaxirasi (Vercel rewrites) 404 manzilga ham 200
    beradi — "soft 404": qidiruv tizimi indekslamasin */}
<meta name="robots" content="noindex" />

Google JS chizgandan keyin noindex ni ko'radi va sahifani indeksga olmaydi. Ehtiyot bo'ling: noindex ni boshqa sahifadan "yuqtirib" olmang. React 19 uni sahifadan chiqqanda olib tashlaydi — shuning uchun komponent ichida yozish xavfsiz.

Tekshirib ko'ring: Nega noindex ni index.html ga yozib, keyin JS bilan "kerakli sahifalarda olib tashlash" yomon g'oya?

Javob

Googlebot xom HTML'da noindex ni ko'rsa, sahifani render navbatiga umuman qo'ymasligi mumkin — JS hech qachon bajarilmaydi va "olib tashlash" ishlamaydi. Google hujjati ham shuni ogohlantiradi: noindex ni JS bilan qo'shish mumkin, olib tashlashga tayanmang. Kanon aksincha qiladi: HTML'da noindex yo'q, faqat 404 sahifa uni qo'shadi.

7. sitemap.xml va robots.txt

Sitemap — saytdagi sahifalar ro'yxati (XML). SPA'da Google havolalarni JS bajarilgandan keyin topadi; sitemap esa ularni darhol beradi. Ko'p tilli saytda sitemap hreflang'ni ham berishi mumkin. Marshrutlar ro'yxatidan build vaqtida yaratish — oddiy Node skripti:

js
const SAYT = "https://example.com/menyu";
const TILLAR = [["uz", ""], ["ru", "/ru"], ["en", "/en"]];
const SAHIFALAR = ["/", "/osh", "/manzil"];

const manzil = (prefiks, yol) =>
  SAYT + prefiks + (yol === "/" ? "/" : yol);

function sitemap() {
  const qatorlar = [];
  for (const yol of SAHIFALAR) {
    for (const [, prefiks] of TILLAR) {
      qatorlar.push("<url>", `  <loc>${manzil(prefiks, yol)}</loc>`);
      for (const [til, p] of TILLAR) {
        qatorlar.push(
          `  <xhtml:link rel="alternate" hreflang="${til}"` +
            ` href="${manzil(p, yol)}"/>`,
        );
      }
      qatorlar.push("</url>");
    }
  }
  return qatorlar;
}

const q = sitemap();
console.log("url:", q.filter((x) => x === "<url>").length);
console.log(q.slice(0, 6).join("\n"));

Konsolda:

text
url: 9
<url>
  <loc>https://example.com/menyu/</loc>
  <xhtml:link rel="alternate" hreflang="uz" href="https://example.com/menyu/"/>
  <xhtml:link rel="alternate" hreflang="ru" href="https://example.com/menyu/ru/"/>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/menyu/en/"/>
</url>

3 sahifa × 3 til = 9 ta <url>, har birida uchala til. To'liq faylga <?xml …?> sarlavhasi va <urlset xmlns="…" xmlns:xhtml="…"> o'rami qo'shiladi (sitemaps.org protokoli). Faylni dist/sitemap.xml ga yozib, robots.txt da ko'rsatasiz: Sitemap: https://…/sitemap.xml.

vazifalar da sitemap ham, robots.txt ham yo'q (TEXNIK-QARZ 69). Sabab: ilovaning ommaviy sahifasi faqat /kirish; qolgani kirish ortida — qidiruvga keraksiz. robots.txt esa domen ildizida bo'lishi kerak (login.github.io/robots.txt), loyiha sayti (login.github.io/vazifalar/) uni o'zi qo'ya olmaydi.

8. Prerender, SSR va SSG: qachon SPA yetmaydi

8.1 Uch yo'l

Yo'l HTML qachon yaratiladi Misol
CSR (SPA) brauzerda, JS bilan vazifalar
SSG / prerender build vaqtida, har sahifa uchun tayyor fayl blog, menyu sayti, hujjatlar
SSR har so'rovda serverda do'kon, yangiliklar, shaxsiy sahifalar

Prerender (build vaqtida oldindan chizish) — SPA'ni o'zgartirmasdan, build oxirida har marshrutni Node'da chizib, dist/osh/index.html kabi fayllarga yozish. Brauzer avval tayyor HTML'ni ko'radi (bot ham!), keyin React uni "jonlantiradi" (hydration). Bu atama 18-qismdagi <Activity> prerender'idan farq qiladi: u yerda React yashirin UI'ni xotirada oldindan chizardi, bu yerda — build'da HTML faylga.

Vite uchun vositalar (hujjat bo'yicha): vite-prerender-plugin — har marshrut uchun prerender() funksiyangizni chaqirib HTML yozadi; vite-ssg — asosan Vue uchun, React uchun uning vite-react-ssg varianti. React'ning o'zida ham build vaqtida chizish API'si bor: react-dom/static dagi prerender (React 19). Next.js va Astro esa SSG'ni "qutidan" beradi. Bu vositalarning hech biri kanonda sinab ko'rilmagan — ular haqidagi gaplar faqat hujjatga tayanadi.

8.2 Kanon: prerender o'lchanmadi

Halol holat: vazifalar da prerender sinab ko'rilmadi va o'lchanmadi — bu darsda "prerender LCP'ni shuncha yaxshiladi" degan raqam yo'q. Sabab: ilova kirish ortida. Prerender qilinadigan ommaviy sahifa — faqat /kirish; ro'yxat, statistika va sozlamalar foydalanuvchining shaxsiy ma'lumoti, ularni oldindan chizib bo'lmaydi. Qaror — TEXNIK-QARZ 69: SSG/SSR kerak bo'lgan joy — 23-qismdagi Next.js portfolio.

8.3 Qaror daraxti

flowchart TB
  A{"Sahifa qidiruvda chiqishi kerakmi?"} -->|yo'q, kirish ortida| B["SPA yetadi: title, noindex 404"]
  A -->|ha| C{"Mazmun build'da ma'lummi?"}
  C -->|ha: menyu, blog| D["SSG / prerender: Astro, Next.js"]
  C -->|yo'q: narx, qoldiq| E["SSR: Next.js"]
  D --> F["har sahifaga o'z og:*, sitemap"]
  E --> F

Diagrammaga qarang: birinchi savol texnologiya haqida emas, mahsulot haqida. «Bahor»ning ichki ilovasi — SPA. Menyu sayti — SSG (Astro ham, Next.js ham yaxshi). Onlayn buyurtma va narxlar har daqiqa o'zgaradigan do'kon — SSR.

Astro — mazmunli saytlar uchun framework: sahifa sukut bo'yicha JS'siz HTML, faqat interaktiv qismlar ("orollar") React bilan (orol g'oyasi). Next.js — React'ning to'liq framework'i: SSG, SSR va server komponentlar bitta loyihada. Uni 23-qismda o'rganamiz.

9. Vazifalar qadami: SEO teglari (holat 32 = v11)

bash
git switch -c feat/seo

O'zgarishlar (kanon):

  • src/komponentlar/SeoTeglari.tsx — «Kanon: SeoTeglari.tsx» bo'limidagi komponent; Qobiq.tsx da <SeoTeglari />.
  • TopilmadiSahifasi.tsx — <meta name="robots" content="noindex" />.
  • index.html — statik og:* (to'rt teg).
  • Hujjatlar: README (v11 bo'limi), AGENTS.md («21-qism qoidalari»), TEXNIK-QARZ v11 (60–70 qatorlar).

Tekshiruvlar (haqiqiy, holat 32):

  • Build: birinchi yuklash JS 206.7 KB (holat 31 — 206.6; SEO komponenti +0.1), CSS 5.1.
  • npm test — node:test 255, Vitest 65; lint, format:check, tip, knip — toza.
  • e2e-21 — 81. SEO bo'limi (natija-21-32.txt):
text
✅ JS'siz: lang, title, description, og:title/description/type: [true,true,"React'da yozilgan vazifalar ro'yxati.","Vazifalar",true,"website"]
✅ JS'siz: canonical/hreflang YO'Q (faqat JS bilan): false
✅ JS bilan: canonical (so'rovsiz), hreflang 4 til + x-default, <head> da, title: {"canonical":"http://127.0.0.1:5718/ru/statistika","alt":[["uz","/statistika"],["uz-Cyrl","/uz-cyrl/statistika"],["ru","/ru/statistika"],["en","/en/statistika"],["x-default","/statistika"]],"title":"Статистика — Задачи","ichida":true}
✅ sahifa almashdi — canonical ham (bitta): ["/ru/sozlamalar"]
✅ 404 sahifa: robots noindex (soft 404): "noindex"
✅ konsolda xato yo'q: []

Ikkinchi qatorga e'tibor bering — test halol cheklovni ham tekshiradi: JS'siz HTML'da canonical va hreflang yo'q. Uchinchi qator — ruscha statistika sahifasi: canonical o'zi, beshta alternate, ruscha sarlavha. To'rtinchisi — sahifa almashganda canonical bitta qoladi (eskisi olib tashlandi).

bash
git add -A
git commit

Commit (kanon): feat: canonical, hreflang, 404 noindex, og:*; v11 hujjatlari.

10. 21-qism yakuni: v10 → v11

Bu dars bilan vazifalar v11 tayyor. 21-qism davomida nima o'zgardi (kanon yakuniy holati):

Ko'rsatkich v10 v11
Birinchi yuklash JS / CSS (gzip) 201.8 / 4.6 KB 206.7 / 5.1 KB
node:test / Vitest 203 / 65 255 / 65
Brauzer testlari 159, 72, 100, 77 159, 72, 100, 77 + e2e-21 81 + dev-21 15
Lighthouse mobil (kirgan / yangi, bir sessiyada) 89 / 93 88 / 92 (holat 31)

Lighthouse qatoriga e'tibor: 20-qism oxirida v10 uchun 93 / 94 yozilgan edi. Bu yerdagi 89 / 93 — o'sha kodning boshqa sessiyadagi o'lchovi; v10 va v11 bitta sessiyada o'lchangani uchun faqat ular o'zaro solishtiriladi (Yuklanishni optimallashtirish darsidagi izoh).

Accessibility, Best Practices, SEO — 100; axe — 0 buzilish (21-qismda 6 ta yangi ko'rinish qo'shildi); CLS — 0.

Kirdi:

  • to'rt til — react-i18next, til manzilda, kirill skript bilan;
  • Markdown izoh (kech, XSS 0), o'z SVG teglar grafigi, rasm kesish;
  • CSV eksport/import, QR (kech), «Joyini tanlab saqlash»;
  • o'z analitika va xatolar kuzatuvi (production'da manzilsiz), feature flag'lar;
  • AI yordamchi (dev'da soxta oqim), SW «Yangilash», oflayn navbat;
  • kech sahifalar, canonical va hreflang.

O'lchandi, lekin kirmadi:

  • Recharts (+103.4 KB va tsc xatolari), posthog (Trusted Types), Sentry (DSN = hisob);
  • vite-plugin-pwa va Serwist (ro'yxatdan o'tkazgich Trusted Types'da);
  • react-easy-crop (innerHTML), Vercel AI SDK (+44.1 KB, Trusted Types);
  • jami 58 ta kutubxona misoli CSP ostida o'lchandi.

Halol qarzlar: web push lokal yetkazilmadi; prerender o'lchanmadi; oflayn navbat faqat navigator.onLine bo'yicha; i18n +17 KB birinchi yuklashda.

Testlar topgan haqiqiy xatolar — 21-qismning eng muhim saboqlaridan biri. e2e va o'lchov skriptlari quyidagilarni topdi, ular tuzatildi:

  • skip link boshqa sahifalarda yo'q joyga ko'rsatardi (v10 dan beri);
  • izoh va rasm kesish oynasi yopilganda fokus yo'qolardi;
  • GFM katakchalarida yorliq yo'q edi (axe — critical);
  • deploydan keyin ochiq sahifa kech chunk'ni ocholmasdi (PWA);
  • Chrome'da o'zbek lotin Intl sanasi yo'q;
  • analitika visibilitychange ni window da tinglardi;
  • soxta AI serverida req "close" darhol kelardi (AI chat).

Bularning hech birini qo'lda sinash topmagan bo'lardi. Shu qism davomida biz testlarni "sahna ortida" yozdik: puppeteer skriptlari, node:test, axe. Keyingi qismda ularni o'zingiz yozishni o'rganasiz: Vitest, React Testing Library, MSW bilan API'ni soxtalashtirish, haqiqiy brauzerda komponent testi va Playwright bilan e2e. vazifalar ning testlari endi sizning qo'lingizda bo'ladi.

11. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Telegram havolasida eski/umumiy sarlavha og:* JS bilan qo'yilgan og:* — HTML'da statik (yoki prerender)
<head> da ikki canonical teg qo'lda appendChild qilingan React 19 <link> — komponentda
hreflang e'tiborsiz o'zaro emas yoki o'zini ko'rsatmaydi har til — hamma tillar + x-default
?filtr=… sahifalari indeksda canonical so'rov bilan pathname — so'rovsiz
404 sahifalar qidiruvda soft 404 (200) noindex
<title> ogohlantirish beradi ikki bola ({x} — Bahor) bitta satr: {`${x} — Bahor`}

12. Mashqlar

Mashqlar kurs/mashqlar/21/32-spa-seo/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Sahifaning asl manzilini ko'rsatuvchi link — rel=""
  • Boshqa tildagi nusxani ko'rsatuvchi atribut —
  • Til mos kelmaganda ko'rsatiladigan qiymat —
  • Soft 404 sahifaga qo'yiladigan robots qiymati —
  • Telegram havola ko'rinishi uchun sarlavha — og:
Yechim
  • canonical — dublikatlar orasida asli.
  • hreflang — React'da hrefLang.
  • x-default — vazifalar da o'zbekcha sahifa.
  • noindex — indeksga olinmasin.
  • title — og:title, HTML'da statik.

2-mashq (o'rta): canonical manzil

Funksiya canonical(manzil) yozing: to'liq manzildan canonical yasaydi — so'rov qismi (?…) va #… olib tashlanadi, yo'l kichik harfga o'tadi, oxirgi / olib tashlanadi (faqat ildiz / qoladi). Tekshiring:

js
console.log(canonical("https://example.com/Menyu/Osh/?utm=tg#narx"));
// https://example.com/menyu/osh

Ishora: URL obyekti — origin va pathname alohida.

Yechim
js
function canonical(manzil) {
  const url = new URL(manzil);
  let yol = url.pathname.toLowerCase();
  if (yol.length > 1 && yol.endsWith("/")) yol = yol.slice(0, -1);
  return url.origin + yol;
}

console.log(canonical("https://example.com/Menyu/Osh/?utm=tg#narx"));
console.log(canonical("https://example.com/?filtr=faol"));
console.log(canonical("https://example.com/ru/statistika"));

Konsolda:

text
https://example.com/menyu/osh
https://example.com/
https://example.com/ru/statistika

url.origin so'rov va # ni o'zi qoldirib ketadi. Kichik harf — saytingiz qoidasi bo'lsa (server ham shunday yo'naltirsa). vazifalar kanoni kichik harfga o'tkazmaydi va chiziqchaga tegmaydi — uning manzillarini router yaratadi, ular allaqachon bir xil.

3-mashq (qiyin): hreflang komponenti

«React 19» bo'limidagi jonli misolga hreflang qo'shing. Tillar: uz (prefikssiz), ru (/ru), en (/en). Talablar:

  1. Har sahifada uch alternate + x-default (o'zbekcha).
  2. Joriy manzil til prefiksi bilan bo'lishi mumkin: /ru/statistika da ham uchala variant to'g'ri (/statistika, /ru/statistika, /en/statistika).
  3. Konsolga <head> dagi alternate lar soni va ru havolasi.

Ishora: avval prefiksni olib tashlang (yol.replace(/^\/(ru|en)(?=\/|$)/, "")), keyin har til prefiksini qo'shing. Bo'sh qolsa — /.

Yechim
jsx
import { useEffect } from "react";
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useLocation,
} from "react-router";

const SAYT = "https://example.com";
const TILLAR = [["uz", ""], ["ru", "/ru"], ["en", "/en"]];

function asosiyYol(yol) {
  return yol.replace(/^\/(ru|en)(?=\/|$)/, "") || "/";
}
function tildaManzil(prefiks, yol) {
  const toliq = prefiks + (yol === "/" ? "" : yol);
  return SAYT + (toliq || "/");
}

function Hreflang() {
  const yol = asosiyYol(useLocation().pathname);
  return (
    <>
      {TILLAR.map(([til, prefiks]) => (
        <link
          key={til}
          rel="alternate"
          hrefLang={til}
          href={tildaManzil(prefiks, yol)}
        />
      ))}
      <link
        rel="alternate"
        hrefLang="x-default"
        href={tildaManzil("", yol)}
      />
    </>
  );
}

function Qobiq() {
  const { pathname } = useLocation();
  useEffect(() => {
    const alt = document.head.querySelectorAll("link[rel=alternate]");
    const ru = document.head.querySelector("link[hreflang=ru]");
    console.log(pathname, alt.length, ru.getAttribute("href"));
  }, [pathname]);
  return (
    <>
      <Hreflang />
      <Link to="/ru/statistika">Ruscha statistika</Link>
      <Outlet />
    </>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Qobiq />,
    children: [
      { index: true, element: <h2>Menyu</h2> },
      { path: "ru/statistika", element: <h2>Статистика</h2> },
    ],
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Konsolda:

text
/ 4 https://example.com/ru
/ru/statistika 4 https://example.com/ru/statistika

Ikkala sahifada ham to'rtta alternate — takror yo'q. Bosh sahifada ru havolasi /ru (oxirida chiziqchasiz — tildaManzil bo'sh yo'lni qo'shmaydi). Ruscha statistikada prefiks olib tashlanib, qayta qo'yildi — /ru/ru/statistika bo'lib qolmadi. Kanon xuddi shu ishni yoldanTil va tildaYol bilan qiladi (i18n II).

4-mashq: Vazifalar qadami

feat/seo branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling va v11 ni yakunlang: README, AGENTS.md, TEXNIK-QARZ.

Yechim

Tekshiruv ro'yxati:

  • npm run build && npm run preview. DevTools → Elements → <head>: /ru/statistika da bitta canonical va beshta alternate.
  • Sahifa manbasi (Ctrl+U) — JS'siz HTML: og:* bor, canonical yo'q (kutilgan).
  • /mavjud-emas — <meta name="robots" content="noindex">.
  • Lighthouse → SEO — 100.
  • npm test — node 255, Vitest 65; birinchi yuklash ~206.7 KB.
  • git tag v11 (kanon tartibi bo'yicha, ixtiyoriy).

13. Real ishda

  • Marketing saytlari (landing, blog, menyu) — SEO asosiy talab; ko'pincha Next.js yoki Astro bilan. Ichki ilovalar (admin, CRM, vazifalar) — SPA, SEO deyarli kerak emas, noindex bilan.
  • Kod ko'rigida savollar: "bu sahifa havola sifatida ulashilsa nima ko'rinadi?", "canonical bitta-mi?", "hreflang o'zaro-mi?".
  • Intervyu savollari: "Google SPA'ni indekslaydimi?", "CSR, SSR, SSG farqi?", "canonical nima uchun?", "soft 404 nima?", "qachon Next.js tanlaysiz?".

Xulosa

  • SPA HTML'i deyarli bo'sh. Google JS'ni navbat bilan bajaradi; havola ko'rinishi va ko'p botlar — bajarmaydi. Havola uchun kerak narsa — HTML'da statik.
  • React 19: <title>, <meta>, <link> — komponentda, <head> ga o'zi ko'tariladi va almashadi (react-helmet-async — eski kodda).
  • Canonical — so'rovsiz manzil; hreflang — har til hamma tillarni, o'zaro, x-default bilan.
  • Soft 404 — noindex; sitemap — marshrutlardan build'da; robots.txt — domen ildizida.
  • Qidiruv kerak bo'lsa: build'da ma'lum mazmun — SSG/prerender, o'zgaruvchan — SSR. vazifalar da prerender o'lchanmadi — ilova kirish ortida.
  • 21-qism: v10 → v11 — to'rt til, oflayn navbat, «Yangilash», kech sahifalar; birinchi yuklash 206.7 KB; testlar 7 ta haqiqiy xatoni topdi.

Keyingi dars: Nega test yozamiz va frontend test turlari — 22-qism boshlanadi: shu qismda sahna ortida ishlagan testlarni endi o'zingiz yozasiz.

Manbalar

  • Google Search Central: "Understand the JavaScript SEO basics", "Consolidate duplicate URLs" (canonical), "Localized versions of your page" (hreflang), soft 404, noindex va JavaScript
  • React 19: <title>, <meta>, <link> komponentlari (react.dev, 2024-12)
  • sitemaps.org: Sitemap protokoli; Google: sitemap'da hreflang (xhtml:link)
  • vite-prerender-plugin, vite-react-ssg, react-dom/static prerender — hujjatlar
  • Kanon: vazifalar holat 32 (= v11), SeoTeglari.tsx, natija-21-32.txt, tekshiruv-32.txt, TEXNIK-QARZ v11 (69) (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
SPA uchun SEO: React ilovani Google qanday ko'radi, canonical, hreflang, sitemap va prerender — IlmHamroh