П/ВИН

Feberra Landing: миграция с Vercel и SEO-оптимизация

·9 мин чтения
Feberra Landing: миграция с Vercel и SEO-оптимизация

Когда проект живёт на Vercel и всё работает само по себе — это удобно. Но рано или поздно приходит момент, когда хочется контроля: своя инфраструктура, единый пайплайн деплоя, предсказуемые расходы и возможность настроить всё по-своему. Именно с такой задачей мы столкнулись с проектом Feberra — AI-платформой для создания видео из текста. Лендинг жил на v0.dev и деплоился через Vercel, а нам нужно было переехать на собственный сервер с Dokploy и заодно провести полноценную SEO-оптимизацию.

В этой статье расскажу, как мы это сделали: от клонирования репозитория до работающего HTTPS на продакшне и полного SEO-аудита с исправлениями.

Контекст: откуда взялся проект

Feberra — это лендинг AI-видеоплатформы. Проект был создан в v0.dev — инструменте от Vercel для быстрого прототипирования интерфейсов на Next.js. Это значит, что в коде присутствовали Vercel-специфичные зависимости (@vercel/analytics), не было Dockerfile, а конфиг Next.js не был настроен для standalone-деплоя.

Репозиторий существовал на GitHub, но не был синхронизирован с нашей рабочей инфраструктурой. Первый шаг — клонирование и аудит того, с чем мы работаем.

После клонирования стало ясно: перед нами стандартный Next.js 15 проект с App Router, несколькими маршрутами (главная + блог), компонентами на shadcn/ui и зависимостью от Vercel-аналитики. Структура чистая, TypeScript строгий — хорошая база для работы.

Проблема: Vercel-специфичный проект без инфраструктурного слоя

Для деплоя на собственный сервер через Dokploy нам не хватало нескольких вещей:

  1. output: "standalone" в next.config — без этого Next.js генерирует полный node_modules в build-артефакте, что делает Docker-образ гигантским
  2. Dockerfile — Dokploy умеет деплоить Docker-образы, но файл нужно создать самим
  3. Удалить @vercel/analytics — этот пакет добавляет скрипт, который обращается к серверам Vercel; на нашей инфраструктуре он бесполезен и может вызывать ошибки

Всё это — типичные шаги миграции с Vercel на любой другой хостинг. Мы уже делали это для нескольких других проектов, поэтому процесс хорошо отработан.

Решение: Dockerfile + Dokploy + автодеплой

Шаг 1: standalone-сборка

Добавляем output: "standalone" в конфиг Next.js:

// next.config.mjs - было
const nextConfig = {
  // пусто или минимальные настройки от v0
};
 
// next.config.mjs - стало
const nextConfig = {
  output: "standalone",
  poweredByHeader: false, // убираем X-Powered-By: Next.js из заголовков
};
 
export default nextConfig;

Опция output: "standalone" заставляет Next.js при сборке копировать только необходимые файлы node_modules — итоговый артефакт становится в разы меньше.

Шаг 2: многоэтапный Dockerfile

Muli-stage build — стандарт для Next.js в Docker. Разделяем зависимости, сборку и финальный образ:

FROM node:20-alpine AS base
 
# Зависимости
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm ci
 
# Сборка
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
 
# Продакшн-образ
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
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"]

Обратите внимание: финальный образ запускается не от root-пользователя — это базовая практика безопасности для production-контейнеров.

Шаг 3: настройка Dokploy

Для подключения проекта к Dokploy используем API: создаём приложение, указываем GitHub-репозиторий, тип сборки (Dockerfile), добавляем домен feberra.com с автоматическим Let's Encrypt сертификатом и настраиваем GitHub webhook для автодеплоя при пуше в main.

После пуша первого коммита с Dockerfile и обновлённым next.config деплой запустился автоматически. DNS уже был направлен на наш VPS, поэтому Traefik (reverse proxy в Dokploy) запросил SSL-сертификат и сайт стал доступен по HTTPS.

Проверяем результат:

curl -I https://feberra.com
# HTTP/2 200
# content-type: text/html

Миграция завершена. Теперь переходим к SEO.

SEO-оптимизация: полный аудит и исправления

После успешного деплоя провели SEO-аудит. Картина была типичной для v0-проектов: базовые meta-теги есть, но всё остальное отсутствует.

Что нашли в ходе аудита

  • Нет sitemap.xml и robots.txt
  • Нет структурированных данных (JSON-LD)
  • Нет Open Graph и Twitter Card тегов
  • Нет PWA-манифеста
  • Нет canonical URLs
  • Нет security headers
  • Нет OG-изображения
  • X-Powered-By: Next.js в ответах сервера (раскрывает стек)
  • Страницы тегов блога индексировались (при дублировании контента — это плохо)

Critical Technical: canonical URLs и security headers

Первым делом — критичные технические проблемы. Страницы /privacy и /terms не имели canonical-тегов. В Next.js App Router это решается через metadata в page.tsx:

// app/privacy/page.tsx
export const metadata: Metadata = {
  title: "Privacy Policy | Feberra",
  alternates: {
    canonical: "https://feberra.com/privacy",
  },
};

Security headers добавляем в next.config.mjs:

const securityHeaders = [
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "X-XSS-Protection", value: "1; mode=block" },
  { 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,
      },
    ];
  },
};

Страницы тегов блога закрываем от индексации через noindex:

// app/blog/tag/[tag]/page.tsx
export const metadata: Metadata = {
  robots: {
    index: false,
    follow: true,
  },
};

On-Page SEO: structured data и метаданные

Для блога добавляем JSON-LD разметку. На странице списка статей — CollectionPage, на отдельной статье — BlogPosting:

// Компонент для JSON-LD на странице статьи
const blogPostingSchema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  description: post.excerpt,
  author: {
    "@type": "Person",
    name: post.author,
  },
  publisher: {
    "@type": "Organization",
    name: "Feberra",
    "@id": "https://feberra.com/#organization",
    logo: {
      "@type": "ImageObject",
      url: "https://feberra.com/logo.png",
    },
  },
  datePublished: post.publishedAt,
  dateModified: post.updatedAt,
  url: `https://feberra.com/blog/${post.slug}`,
  mainEntityOfPage: {
    "@type": "WebPage",
    "@id": `https://feberra.com/blog/${post.slug}`,
  },
};

На главной странице добавляем FAQPage schema (для блока с часто задаваемыми вопросами) и Organization с sameAs ссылками на социальные сети.

Sitemap и robots.txt

Next.js 13+ поддерживает генерацию sitemap прямо из кода:

// app/sitemap.ts
import { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/blog-data";
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
 
  const blogUrls = posts.map((post) => ({
    url: `https://feberra.com/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: "weekly" as const,
    priority: 0.7,
  }));
 
  return [
    {
      url: "https://feberra.com",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 1,
    },
    {
      url: "https://feberra.com/blog",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 0.9,
    },
    ...blogUrls,
  ];
}

Аналогично — robots.ts для генерации robots.txt:

// app/robots.ts
import { MetadataRoute } from "next";
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/api/", "/_next/"],
    },
    sitemap: "https://feberra.com/sitemap.xml",
  };
}

Open Graph и динамическое OG-изображение

Основные Open Graph теги добавляем в layout.tsx:

export const metadata: Metadata = {
  metadataBase: new URL("https://feberra.com"),
  title: {
    default: "Feberra — AI Video Platform",
    template: "%s | Feberra",
  },
  openGraph: {
    type: "website",
    siteName: "Feberra",
    locale: "en_US",
  },
  twitter: {
    card: "summary_large_image",
    site: "@feberra",
  },
};

Динамическое 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 const contentType = "image/png";
 
export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          background: "linear-gradient(135deg, #0f0f0f 0%, #1a1a2e 100%)",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <h1 style={{ color: "white", fontSize: 72, fontWeight: 700 }}>
          Feberra
        </h1>
        <p style={{ color: "#a0a0a0", fontSize: 32 }}>
          Create videos from text with AI
        </p>
      </div>
    ),
    { ...size }
  );
}

GEO: llms.txt для AI-поисковиков

Отдельная интересная задача — добавить llms.txt. Это относительно новый стандарт, который помогает AI-поисковикам (Perplexity, ChatGPT, Claude) лучше понимать структуру сайта. По аналогии с robots.txt, но для LLM.

// app/llms.txt/route.ts
export async function GET() {
  const content = `# Feberra
 
> AI-powered video creation platform. Turn text into professional videos.
 
## Pages
 
- [Home](https://feberra.com): Main landing page
- [Blog](https://feberra.com/blog): Articles about AI video creation
 
## About
 
Feberra helps creators, marketers and businesses create videos from text 
using artificial intelligence. No video editing skills required.
`;
 
  return new Response(content, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Параллельная работа субагентов

Одна из особенностей этого проекта — мы использовали параллельных субагентов для выполнения SEO-задач. 12 задач были разбиты на 5 чанков и запущены параллельно. Часть агентов столкнулась с ограничениями прав доступа и не смогла записать файлы — их задачи пришлось доделывать вручную.

Это интересный кейс: параллельность ускоряет работу, но требует надёжной системы проверки результатов. Нельзя просто запустить агентов и считать задачи выполненными — нужна финальная верификация через git status и npm run build.

После всех правок запускаем билд:

✓ Compiled successfully
Route (app)                    Size
├ ○ /                          12.4 kB
├ ○ /blog                      8.2 kB
├ ● /blog/[slug]               5.1 kB
├ ○ /sitemap.xml               0 B
├ ○ /robots.txt                0 B
├ ○ /llms.txt                  0 B
└ ○ /opengraph-image           0 B

44 pages generated

44 страницы, все маршруты работают, sitemap и robots генерируются статически.

Результат

Что получили в итоге:

  • feberra.com работает на собственном сервере через Dokploy
  • HTTPS с автоматическим обновлением Let's Encrypt сертификата
  • Автодеплой по пушу в main через GitHub webhook — время от коммита до продакшна около 2-3 минут
  • Sitemap.xml с 44 URL-адресами, готов к подаче в Search Console
  • JSON-LD разметка — BlogPosting, CollectionPage, FAQPage, Organization
  • Security headers — X-Content-Type-Options, X-Frame-Options, CSP и другие
  • OG-изображение генерируется динамически
  • llms.txt для AI-поисковиков
  • noindex на страницах тегов (предотвращаем дублирование контента)

Выводы и уроки

Миграция лендинга с Vercel на собственную инфраструктуру — это не страшно и занимает несколько часов, если есть чёткий чеклист. Ключевые вещи: output: "standalone", многоэтапный Dockerfile и правильная настройка reverse proxy. Всё остальное — детали реализации.

SEO-оптимизация v0-проектов — отдельная история. v0 генерирует хороший React-код, но совершенно игнорирует SEO. Нет sitemap, нет structured data, нет security headers — это норма для прототипов, но не для продакшна. Перед запуском любого v0-проекта стоит пройтись по стандартному SEO-чеклисту.

Параллельные субагенты ускоряют работу, но требуют системы верификации. В нашем случае 3 из 4 агентов получили отказ в правах и не смогли записать файлы — без финальной проверки через git diff и npm run build мы бы посчитали задачи выполненными, не сделав ничего реального. Всегда верифицируй результат агентов через реальное состояние файловой системы.

llms.txt — интересный новый стандарт, который стоит добавлять во все публичные проекты. AI-поисковики становятся значимым источником трафика, и структурированная информация о сайте помогает им лучше понимать контент. Затраты минимальны — один route-файл.

Наконец, важный архитектурный момент: poweredByHeader: false — маленькая настройка, которая убирает X-Powered-By: Next.js из HTTP-ответов. Это не безопасность через неизвестность (версию стека всё равно можно определить), но это хорошая гигиена: не раскрывать лишнюю информацию о стеке без необходимости.

Полезные ссылки

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.

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

Feberra