ByteGuide
← Все статьи🇷🇺 Русский

Субдомены в Next.js: Разные layouts для разных доменов

Как настроить разные интерфейсы для основного сайта и субдомена в Next.js. Разбираем реальный кейс: блог на основном домене, минималистичные инструменты на поддомене.

next.jssubdomainsmulti-tenantroutingux
⏱️ 6 мин. чтения

Субдомены в Next.js: Разные layouts для разных доменов

🤔 Проблема

Представьте ситуацию: у вас есть сайт с блогом на example.com, и вы хотите добавить набор инструментов на tools.example.com. Но дизайн для инструментов должен быть минималистичным — без навигации блога, рекламы и лишних элементов.

Вопрос: Как в одном Next.js приложении показывать разные layouts в зависимости от домена?

🎯 Реальный кейс

Требования

  • example.com — блог с полным Header/Footer, навигацией, поиском
  • tools.example.com — инструменты с минимальным UI (логотип + переключатель темы)
  • Один Next.js проект, один Docker контейнер
  • Nginx проксирует оба домена на один порт

Почему разные layouts?

1. Разные цели пользователей

Блог:

  • Цель: читать статьи, искать информацию
  • Время на сайте: 5-10 минут
  • Нужна навигация, категории, поиск

Инструменты:

  • Цель: быстро решить задачу (сгенерировать UUID, закодировать Base64)
  • Время на сайте: 30 секунд
  • Навигация блога отвлекает

2. SEO и конверсия

Google любит специализированные страницы:

✅ Хорошо: Поиск "UUID generator" → чистая страница инструмента
❌ Плохо: Поиск "UUID generator" → страница с меню блога (выше bounce rate)

3. Монетизация

  • На блоге — контентная реклама (Яндекс.Директ, Google AdSense)
  • На инструментах — другая модель (премиум функции, спонсорство)

Разные layouts = разные стратегии монетизации.


🛠️ Решение

Архитектура

example.com       → nginx:443 → Next.js:3000 (RootLayout + BlogHeader/Footer)
tools.example.com → nginx:443 → Next.js:3000 (RootLayout + ToolsHeader/Footer)

Next.js определяет домен через заголовок Host и показывает нужный layout.

Шаг 1: Настройка DNS

Добавьте A-запись или CNAME для субдомена:

# DNS настройки
A     example.com       →  192.168.1.1
A     tools.example.com →  192.168.1.1

Или через CNAME:

CNAME tools  →  example.com

Шаг 2: Nginx конфигурация

Создайте отдельный server block для субдомена:

# /etc/nginx/sites-available/tools.example.com.conf

server {
    listen 443 ssl http2;
    server_name tools.example.com;

    ssl_certificate /etc/letsencrypt/live/tools.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tools.example.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;  # Важно! Передаём правильный Host
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Ключевой момент: proxy_set_header Host $host — это передаёт Next.js информацию о том, с какого домена пришёл запрос.

Шаг 3: SSL сертификаты

# Получите сертификат для субдомена
sudo certbot --nginx -d tools.example.com

Certbot автоматически настроит SSL и добавит редирект с HTTP на HTTPS.

Шаг 4: Next.js — Root Layout

Основной layout проверяет домен и решает, показывать ли Header/Footer:

// app/layout.tsx
import { headers } from "next/headers";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const headersList = await headers();
  const host = headersList.get('host') || '';
  const isToolsSubdomain = host.startsWith('tools.');

  return (
    <html lang="ru">
      <body>
        <ThemeProvider>
          {/* Показываем Header/Footer только если НЕ субдомен tools */}
          {!isToolsSubdomain && <Header />}
          <main className="flex-1">{children}</main>
          {!isToolsSubdomain && <Footer />}
        </ThemeProvider>
      </body>
    </html>
  );
}

Почему async? Потому что headers() — это асинхронная функция в Next.js 15+.

Шаг 5: Tools Layout

Для страниц инструментов создайте отдельный layout:

// app/tools/layout.tsx
import { headers } from "next/headers";
import { ThemeToggle } from "@/components/ThemeToggle";

export default async function ToolsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const headersList = await headers();
  const host = headersList.get('host') || '';
  const isToolsSubdomain = host.startsWith('tools.');

  // Если запрос с tools.example.com — добавляем минималистичный UI
  if (isToolsSubdomain) {
    return (
      <>
        <header className="sticky top-0 border-b">
          <div className="container flex h-14 items-center justify-between">
            <a href="/" className="text-xl font-bold">
              Tools
            </a>
            <nav className="flex items-center gap-3">
              <a href="https://example.com">Блог</a>
              <ThemeToggle />
            </nav>
          </div>
        </header>

        {children}

        <footer className="border-t py-6">
          <div className="container text-center text-sm">
            © 2025 Tools • Все инструменты работают локально
          </div>
        </footer>
      </>
    );
  }

  // Если запрос с example.com/tools — просто children
  // (Header/Footer уже есть в RootLayout)
  return <>{children}</>;
}

Логика:

  • tools.example.com/uuid → минималистичный header/footer
  • example.com/tools/uuid → обычный Header/Footer блога

Шаг 6: Middleware (опционально)

Если нужно более сложное поведение (редиректы, rewrite), используйте middleware:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  
  if (hostname.startsWith('tools.')) {
    const url = request.nextUrl;
    
    // Например, редирект с корня на /tools
    if (url.pathname === '/') {
      url.pathname = '/tools';
      return NextResponse.redirect(url);
    }
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};

🧪 Тестирование на localhost

Проблема

На localhost:3000 у вас нет субдоменов. Как тестировать?

Решение: hosts файл

Windows

  1. Откройте Блокнот от имени администратора
  2. Файл → Открыть → C:\Windows\System32\drivers\etc\hosts
  3. Добавьте:
127.0.0.1 example.localhost
127.0.0.1 tools.localhost

macOS/Linux

sudo nano /etc/hosts

# Добавьте:
127.0.0.1 example.localhost
127.0.0.1 tools.localhost

Тестируйте

npm run dev

Откройте:

  • http://tools.localhost:3000 → минималистичный UI
  • http://example.localhost:3000 → полный Header/Footer

📊 Результат

Что мы получили?

Один Next.js проект — легче поддерживать
Разные UI для разных доменов — лучше UX
SEO оптимизация — специализированные страницы
Гибкая монетизация — разные стратегии для блога и инструментов

Метрики (пример)

До (инструменты на example.com/tools):

  • Bounce rate: 65%
  • Время на сайте: 45 секунд
  • Конверсия в клик по рекламе: 1.2%

После (инструменты на tools.example.com):

  • Bounce rate: 42% ✅
  • Время на сайте: 1 минута 20 секунд ✅
  • Конверсия в клик: 2.8% ✅

🚀 Дополнительные возможности

1. Разная аналитика

// app/layout.tsx
{!isToolsSubdomain && <YandexMetrika id="12345" />}
{isToolsSubdomain && <YandexMetrika id="67890" />}

Отдельные счётчики для блога и инструментов.

2. Разные мета-теги

// app/tools/layout.tsx
export const metadata: Metadata = {
  title: "Developer Tools",
  robots: "index, follow",
  openGraph: {
    type: "website",
    siteName: "Tools",
  },
};

3. A/B тестирование

const showNewDesign = host === 'beta.example.com';
return showNewDesign ? <NewHeader /> : <OldHeader />;

⚠️ Частые ошибки

1. Забыли proxy_set_header Host

# ❌ Плохо
location / {
    proxy_pass http://localhost:3000;
}

# ✅ Хорошо
location / {
    proxy_pass http://localhost:3000;
    proxy_set_header Host $host;  # Обязательно!
}

Без этого Next.js всегда видит localhost:3000.

2. Дублирование Header/Footer

// ❌ Плохо: оба layout рендерят Header
// RootLayout → Header
// ToolsLayout → Header (дубль!)

// ✅ Хорошо: проверяйте домен в RootLayout
{!isToolsSubdomain && <Header />}

3. Кэширование в браузере

После изменений очистите кэш:

# Chrome
Ctrl+Shift+R (Windows)
Cmd+Shift+R (Mac)

🎓 Выводы

Когда использовать субдомены?

✅ Используйте, если:

  • Разные типы контента (блог + инструменты)
  • Разные стратегии монетизации
  • Нужна специализация для SEO
  • Разные целевые аудитории

❌ Не используйте, если:

  • Просто несколько страниц
  • Контент похожий по смыслу
  • Нет ресурсов на поддержку

Альтернативы

  1. Отдельные Next.js проекты — проще, но дороже (два сервера)
  2. Условный рендеринг по путиif (pathname.startsWith('/tools'))
  3. Middleware rewrites — сложнее, но мощнее

📚 Ссылки