П/ВИН

Миграция feberra.com с Vercel на Dokploy и SEO

·8 мин чтения

Иногда проект существует, но ты о нём не знаешь. Именно так получилось с 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. Пишу о реальных кейсах из продакшена.

Связанный проект

Смотреть в портфолио →