Субдомены в Next.js: Разные layouts для разных доменов
Как настроить разные интерфейсы для основного сайта и субдомена в Next.js. Разбираем реальный кейс: блог на основном домене, минималистичные инструменты на поддомене.
Субдомены в 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/footerexample.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
- Откройте Блокнот от имени администратора
- Файл → Открыть →
C:\Windows\System32\drivers\etc\hosts - Добавьте:
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→ минималистичный UIhttp://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
- Разные целевые аудитории
❌ Не используйте, если:
- Просто несколько страниц
- Контент похожий по смыслу
- Нет ресурсов на поддержку
Альтернативы
- Отдельные Next.js проекты — проще, но дороже (два сервера)
- Условный рендеринг по пути —
if (pathname.startsWith('/tools')) - Middleware rewrites — сложнее, но мощнее