Миграция feberra.com с Vercel на Dokploy и SEO
Иногда проект существует, но ты о нём не знаешь. Именно так получилось с feberra-landing — лендингом AI-платформы для генерации видео из текста. Проект был создан в v0 от Vercel, задеплоен на feberra.com, но ни разу не синхронизировался с нашей инфраструктурой. Никакого Dockerfile, никакого Dokploy, никакой SEO-оптимизации. Просто Vercel-деплой «как есть».
В этой статье расскажу, как мы за одну сессию клонировали репозиторий, переехали на собственный сервер с Dokploy, полностью оптимизировали сайт под поисковики и настроили автодеплой по пушу в main.
Контекст проекта
Feberra — AI-платформа для создания видео из текста. Лендинг живёт по адресу feberra.com и представляет собой стандартный Next.js-проект, сгенерированный в v0.dev. Репозиторий нашёлся на GitHub: bugle-c/feberra-landing-page.
Структура типичная для v0-проектов:
- Next.js 15 (App Router)
- TypeScript
- Tailwind CSS
@vercel/analytics— пакет, который нужен только на Vercel- Никакого
output: "standalone", никакого Dockerfile
Наша задача: перенести всё это на собственный VPS с Dokploy, настроить HTTPS, автодеплой и сделать нормальную SEO-оптимизацию.
Шаг 1. Клонирование и аудит
Первым делом клонируем репозиторий на сервер и смотрим, что внутри:
git clone https://github.com/bugle-c/feberra-landing-page
cd feberra-landing-page
ls -laКлючевые находки из аудита:
next.config.mjs— нетoutput: "standalone", без которого Docker-образ будет весить сотни мегабайтpackage.json— есть@vercel/analytics, который вне Vercel просто занимает место- Нет
Dockerfileвообще layout.tsx— базовые мета-теги, нет OG-разметки, нет structured data- Нет
robots.txt, нетsitemap.xml
Картина типичная для v0-проектов: быстро запустили, красиво выглядит, но под капотом — пусто.
Шаг 2. Подготовка к контейнеризации
Для деплоя через Dokploy нам нужен Docker-образ. Next.js поддерживает режим standalone, который копирует только необходимые файлы — это критично для размера образа.
Обновляем next.config.mjs
// before
const nextConfig = {
// пусто или минимальные настройки
};
// after
const nextConfig = {
output: "standalone",
poweredByHeader: false, // убираем X-Powered-By: Next.js
};
export default nextConfig;poweredByHeader: false — маленькая деталь для безопасности: не раскрываем стек атакующим.
Убираем @vercel/analytics
npm uninstall @vercel/analyticsИ чистим layout.tsx от импорта Analytics. На Dokploy этот пакет просто не работает, зато добавляет зависимость и потенциальные ошибки при сборке.
Создаём Dockerfile
Используем проверенный multi-stage паттерн на node:20-alpine:
# Stage 1: dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]Три стадии — это стандарт: отдельно зависимости, отдельно сборка, финальный минимальный образ. В результате итоговый образ весит ~150MB вместо 1GB+.
Шаг 3. Деплой через Dokploy
Dokploy — наш self-hosted аналог Vercel/Railway. Он живёт на VPS, управляет Docker-контейнерами, Traefik-прокси и автоматически получает Let's Encrypt сертификаты.
Создаём приложение через Dokploy API, указываем:
customGitUrl— ссылка на GitHub-репозиторийbuildType: "dockerfile"— используем наш Dockerfile- Домен
feberra.comс HTTPS - GitHub webhook для автодеплоя
После настройки запускаем первый деплой вручную и проверяем:
curl -I https://feberra.com
# HTTP/2 200
# Сайт работает!DNS уже был направлен на наш VPS (A-запись на IP сервера). Traefik автоматически запросил SSL-сертификат через Let's Encrypt — всё заняло буквально минуту.
С этого момента любой пуш в ветку main автоматически триггерит пересборку и деплой. Никакого ручного вмешательства.
Шаг 4. Полная SEO-оптимизация
Это была самая объёмная часть работы. Аудит показал, что v0 генерирует красивый HTML, но с точки зрения поисковиков сайт практически невидим.
Что отсутствовало изначально
- Нет
sitemap.xml— поисковики не знают о страницах - Нет
robots.txt— нет инструкций для краулеров - Нет OG-тегов — при расшаривании в соцсетях ничего не показывается
- Нет structured data (JSON-LD) — нет шанса на rich snippets
- Нет
manifest.webmanifest— сайт не оптимизирован для мобильных - Дублирующийся
<h1>на некоторых страницах - Нет canonical URLs — риск дублей для поисковиков
- Нет security headers
sitemap.xml и robots.txt
В Next.js App Router это делается элегантно через специальные файлы:
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://feberra.com',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
{
url: 'https://feberra.com/blog',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8,
},
// ... остальные страницы
];
}// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://feberra.com/sitemap.xml',
};
}Next.js сам генерирует эти файлы при билде — никаких статических XML-файлов, которые нужно обновлять вручную. Подробнее в документации Next.js по Metadata.
Structured Data (JSON-LD)
Structured data — это способ объяснить поисковикам, что именно находится на странице. Добавляем на главную:
// Организация
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://feberra.com/#organization',
name: 'Feberra',
url: 'https://feberra.com',
logo: 'https://feberra.com/logo.png',
sameAs: [
'https://twitter.com/feberra',
// другие соцсети
],
};
// FAQ для rich snippets
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems.map(item => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};Для блога добавляем BlogPosting schema на каждую статью и CollectionPage на листинг. Это даёт шанс на появление в Google Discover и расширенных сниппетах.
OG-изображение через Next.js
Вместо статической картинки — динамическая генерация через ImageResponse:
// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = { width: 1200, height: 630 };
export default function Image() {
return new ImageResponse(
<div style={{
background: 'linear-gradient(135deg, #0f0f1a 0%, #1a0a2e 100%)',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}>
<h1 style={{ color: 'white', fontSize: 72, fontWeight: 700 }}>
Feberra
</h1>
<p style={{ color: '#a78bfa', fontSize: 36 }}>
AI Video Generation Platform
</p>
</div>
);
}Security Headers
В next.config.mjs добавляем заголовки безопасности:
const securityHeaders = [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
];
const nextConfig = {
output: 'standalone',
poweredByHeader: false,
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};Эти заголовки влияют не только на безопасность, но и на оценку сайта в Google PageSpeed и Security аудитах.
noindex для технических страниц
Теговые страницы блога (/blog/tag/[tag]) не должны индексироваться — это потенциальные дубли. Добавляем в metadata:
export const metadata: Metadata = {
robots: {
index: false,
follow: true,
},
};llms.txt для GEO
GEO (Generative Engine Optimization) — новая область оптимизации для AI-поисковиков типа ChatGPT, Perplexity, Claude. Файл llms.txt — это как robots.txt, но для языковых моделей:
# Feberra
> AI-платформа для создания видео из текста
## Что такое Feberra
Feberra — это...
## Ключевые возможности
- Генерация видео из текстового описания
- ...
Route в Next.js:
// app/llms.txt/route.ts
export async function GET() {
const content = `# Feberra\n\n> AI Video Generation...`;
return new Response(content, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}Результат
После всех изменений билд прошёл чисто: 44 страницы сгенерированы статически, динамические роуты работают. Финальная проверка:
curl https://feberra.com/sitemap.xml
# <?xml version="1.0"...> — работает
curl https://feberra.com/robots.txt
# User-agent: * — работает
curl -I https://feberra.com/opengraph-image
# HTTP/2 200, content-type: image/png — работает
curl https://feberra.com/llms.txt
# # Feberra... — работаетВесь процесс от клонирования до полностью оптимизированного продакшн-деплоя занял одну рабочую сессию.
Чему научил этот проект
Первый и главный урок: v0 — отличный инструмент для прототипирования, но не для продакшна. Сгенерированный код визуально красив и функционален, но в нём нет ни SEO-инфраструктуры, ни Docker-поддержки, ни security headers. Всё это нужно добавлять руками. Хорошая новость — это делается по чеклисту, и со временем чеклист автоматизируется.
Второй урок: output: "standalone" в Next.js — это не опция, это обязательный параметр для любого Docker-деплоя. Без него образ раздувается до 1GB+ и деплой занимает вечность. С ним — компактный образ, быстрый запуск. Подробнее в документации Next.js по Docker.
Третий урок: SEO — это не одна задача, а система. Sitemap, robots, canonical URLs, structured data, OG-теги, security headers, noindex на технических страницах — всё это работает вместе. Пропустишь одно звено — получишь проблему. Мы используем чеклист из 12 пунктов, разбитый на приоритетные чанки: сначала критические технические исправления, потом on-page SEO, потом GEO для AI-поисковиков.
Четвёртый урок: параллельное выполнение задач работает, но требует контроля. Мы запускали 4 субагента параллельно для разных чанков SEO-оптимизации. Агенты 1, 3 и 4 столкнулись с проблемами разрешений, только агент 2 отработал чисто. Итог: параллелизм ускоряет работу, но финальная верификация — всегда вручную. git diff и npm run build перед коммитом — обязательны.
Пятый урок, который выходит за рамки этого конкретного деплоя: KNOWLEDGE.md проектов имеет тенденцию к росту, и с этим нужно работать системно. Когда файл памяти проекта вырастает до 900 строк, он перестаёт быть полезным — никто не читает 900 строк перед каждой задачей. Решение — индексный файл плюс специализированные файлы для архитектуры, debugging-истории и инфраструктуры. Но это тема отдельной статьи.
Ссылки

Фулстек-разработчик, строю SaaS-продукты и автоматизации на Next.js, Python и AI. Пишу о реальных кейсах из продакшена.
Связанный проект
Смотреть в портфолио →