Автор:
    Дата створення:2025-09-28Останнє оновлення:2026-09-27

    SEO та i18n в Next.js: Перекладу недостатньо

    Коли розробники думають про інтернаціоналізацію (i18n), першим рефлексом часто є: перекласти контент. Але зазвичай забувають, що головна мета інтернаціоналізації, зробити ваш вебсайт більш помітним для світу. Якщо ваша багатомовна програма Next.js не повідомляє пошуковим системам, як сканувати й розуміти різні мовні версії, більшість ваших зусиль може залишитися непоміченою.

    У цьому блозі ми розглянемо, чому i18n, це суперсила для SEO, і як правильно реалізувати її в Next.js за допомогою next-intl, next-i18next та Intlayer.

    Чому SEO та i18n

    Додавання мов, це не лише про UX. Це також потужний важіль для органічної видимості. Ось чому:

    1. Краща виявлюваність: Пошукові системи індексують локалізовані версії та ранжують їх для користувачів, які шукають рідною мовою.
    2. Уникнення дубльованого контенту: Правильні канонічні та альтернативні теги повідомляють сканерам, яка сторінка належить до якої локалі.
    3. Кращий UX: Відвідувачі потрапляють одразу на потрібну версію вашого сайту.
    4. Конкурентна перевага: Лише небагато сайтів правильно реалізують багатомовне SEO, тож ви можете виділитися.

    Найкращі практики для багатомовного SEO в Next.js

    Ось контрольний список, який має реалізувати кожен багатомовний додаток:

    • Встановіть мета-теги hreflang у <head>
      Допомагає Google зрозуміти, які версії існують для кожної мови.

    • Перелічіть всі перекладені сторінки у sitemap.xml
      Використовуйте схему xhtml, щоб пошукові роботи могли легко знаходити альтернативні версії.

    • Виключіть приватні/локалізовані маршрути в robots.txt
      Наприклад, не дозволяйте індексацію /dashboard, /fr/dashboard, /es/dashboard.

    • Використовуйте локалізовані посилання
      Приклад: <a href="/fr/about">Про нас</a> замість посилання на сторінку за замовчуванням /about.

    Це прості кроки, але їх пропуск може коштувати вам видимості.

    Приклади реалізації

    Розробники часто забувають правильно посилатися на свої сторінки для різних локалей, тож давайте подивимось, як це працює на практиці з різними бібліотеками.

    next-intl

    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";
    import { locales, defaultLocale } from "@/i18n";
    import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
    
    function localizedPath(locale: string, path: string) {
      return locale === defaultLocale ? path : `/${locale}${path}`;
    }
    
    export async function generateMetadata({
      params,
    }: {
      params: { locale: string };
    }): Promise<Metadata> {
      const { locale } = params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const url = "/about";
      const languages = Object.fromEntries(
        locales.map((l) => [l, localizedPath(l, url)])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, url),
          languages: { ...languages, "x-default": url },
        },
      };
    }
    
    // ... решта коду сторінки
    
    src/app/sitemap.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    
    const formatterLocalizedPath = (locale: string, path: string) =>
      locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const aboutLanguages = Object.fromEntries(
        locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
      );
    
      return [
        {
          url: formatterLocalizedPath(defaultLocale, "/about"),
          lastModified: new Date(),
          changeFrequency: "monthly",
          priority: 0.7,
          alternates: { languages: aboutLanguages },
        },
      ];
    }
    
    src/app/robots.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    const withAllLocales = (path: string) => [
      path,
      ...locales.filter((l) => l !== defaultLocale).map((l) => `/${l}${path}`),
    ];
    
    export default function robots(): MetadataRoute.Robots {
      const disallow = [
        ...withAllLocales("/dashboard"),
        ...withAllLocales("/admin"),
      ];
    
      return {
        rules: { userAgent: "*", allow: ["/"], disallow },
        host: origin,
        sitemap: `${origin}/sitemap.xml`,
      };
    }
    

    next-i18next

    i18n.config.ts
    export const locales = ["en", "fr"] as const;
    export type Locale = (typeof locales)[number];
    export const defaultLocale: Locale = "en";
    
    /** Додає префікс локалі до шляху, якщо це не локаль за замовчуванням */
    export function localizedPath(locale: string, path: string) {
      return locale === defaultLocale ? path : `/${locale}${path}`;
    }
    
    /** Допоміжна функція для абсолютних URL */
    const ORIGIN = "https://example.com";
    export function abs(locale: string, path: string) {
      return `${ORIGIN}${localizedPath(locale, path)}`;
    }
    
    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";
    import { locales, defaultLocale, localizedPath } from "@/i18n.config";
    
    export async function generateMetadata({
      params,
    }: {
      params: { locale: string };
    }): Promise<Metadata> {
      const { locale } = params;
    
      // Динамічно імпортуємо відповідний JSON-файл
      const messages = (await import(`@/../public/locales/${locale}/about.json`))
        .default;
    
      const languages = Object.fromEntries(
        locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: messages.title,
        description: messages.description,
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    }
    
    export default async function AboutPage() {
      return <h1>Про нас</h1>;
    }
    
    src/app/sitemap.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale, abs } from "@/i18n.config";
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const languages = Object.fromEntries(
        locales.map((l) => [l, abs(l, "/about")])
      );
      return [
        {
          url: abs(defaultLocale, "/about"),
          lastModified: new Date(),
          changeFrequency: "monthly",
          priority: 0.7,
          alternates: { languages },
        },
      ];
    }
    
    src/app/robots.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale, localizedPath } from "@/i18n.config";
    
    const ORIGIN = "https://example.com";
    
    const expandAllLocales = (path: string) => [
      localizedPath(defaultLocale, path),
      ...locales
        .filter((l) => l !== defaultLocale)
        .map((l) => localizedPath(l, path)),
    ];
    
    export default function robots(): MetadataRoute.Robots {
      const disallow = [
        ...expandAllLocales("/dashboard"),
        ...expandAllLocales("/admin"),
      ];
    
      return {
        rules: { userAgent: "*", allow: ["/"], disallow },
        host: ORIGIN,
        sitemap: `${ORIGIN}/sitemap.xml`,
      };
    }
    

    Intlayer

    src/app/[locale]/about/layout.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
    
      const metadata = getIntlayer("page-metadata", locale);
    
      /**
       * Генерує об'єкт, що містить усі URL-адреси для кожної локалі.
       *
       * Приклад:
       * ```ts
       *  getMultilingualUrls('/about');
       *
       *  // Повертає
       *  // {
       *  //   en: '/about',
       *  //   fr: '/fr/about',
       *  //   es: '/es/about',
       *  // }
       * ```
       */
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    
    // ... Решта коду сторінки
    
    src/app/sitemap.ts
    import { getMultilingualUrls } from "intlayer";
    import type { MetadataRoute } from "next";
    
    const sitemap = (): MetadataRoute.Sitemap => [
      {
        url: "https://example.com/about",
        alternates: {
          languages: { ...getMultilingualUrls("https://example.com/about") },
        },
      },
    ];
    
    src/app/robots.ts
    import { getMultilingualUrls } from "intlayer";
    import type { MetadataRoute } from "next";
    
    const getAllMultilingualUrls = (urls: string[]) =>
      urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
    
    const robots = (): MetadataRoute.Robots => ({
      rules: {
        userAgent: "*",
        allow: ["/"],
        disallow: getAllMultilingualUrls(["/dashboard"]),
      },
      host: "https://example.com",
      sitemap: `https://example.com/sitemap.xml`,
    });
    
    export default robots;
    
    Intlayer надає функцію getMultilingualUrls для генерації багатомовних URL-адрес для вашого sitemap.

    Перевірте свій веб-сайт за допомогою Intlayer SEO Scanner

    Щоб переконатися, що ваш багатомовний додаток ідеально оптимізовано, ми рекомендуємо використовувати Intlayer SEO Scanner. Це спеціалізований інструмент для перевірки багатомовних веб-сайтів, який допомагає виявити відсутні теги hreflang, непрацюючі посилання та інші проблеми SEO на всіх ваших локалізованих сторінках.

    Висновок

    Правильна реалізація i18n у Next.js, це не просто переклад тексту, а забезпечення того, щоб пошукові системи та користувачі точно знали, яку версію вашого контенту показувати. Налаштування hreflang, sitemap і правил для robots, те, що перетворює переклади на реальну SEO-цінність.

    Хоча next-intl і next-i18next дають надійні способи це реалізувати, зазвичай вони вимагають багато ручного налаштування, щоб підтримувати консистентність між локалями.

    Саме тут Intlayer дійсно вирізняється:

    Воно постачається з вбудованими хелперами, такими як getMultilingualUrls, що робить інтеграцію hreflang, sitemap і robots майже беззусильною.

    Метадані зберігаються централізовано замість того, щоб розкидуватися по JSON-файлах або власних утилітах.

    Він спроєктований для Next.js з нуля, тож ви витрачаєте менше часу на налагодження конфігурації й більше, на реліз.

    Якщо ваша мета, не просто перекладати, а масштабувати багатомовне SEO без зайвих зусиль, Intlayer дає вам найчистіше, найбільш стійке до майбутніх змін налаштування.

    Щоб зрозуміти, звідки взялися ці бібліотеки, прочитайте історію i18n у JavaScript.

    Коментарі

    Поки що немає коментарів. Будьте першим, хто поділиться своїми думками.

    Схожі публікації

    Останні публікації