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 нам не хватало нескольких вещей:
output: "standalone"в next.config — без этого Next.js генерирует полный node_modules в build-артефакте, что делает Docker-образ гигантским- Dockerfile — Dokploy умеет деплоить Docker-образы, но файл нужно создать самим
- Удалить
@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-ответов. Это не безопасность через неизвестность (версию стека всё равно можно определить), но это хорошая гигиена: не раскрывать лишнюю информацию о стеке без необходимости.
Полезные ссылки
- Next.js Standalone Output — документация по
output: "standalone" - Next.js Metadata — полное руководство по SEO в App Router
- Schema.org JSON-LD — спецификация структурированных данных
- Dokploy Documentation — документация по self-hosted деплою
- llms.txt specification — стандарт для AI-поисковиков

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