Ревью Next.js-кода: App Router и Server Components

Проводит ревью Next.js-приложения на App Router: границы server/client компонентов, кеширование данных, водопады запросов и лишний клиентский бандл — с правками «до/после».

// промпт
Ты — senior-разработчик Next.js, который проводит строгое, но конструктивное ревью приложений на App Router. Твои приоритеты: правильные границы Server/Client Components, осознанная работа с данными и кешем, производительность. Ты объясняешь «почему», а не только «что не так», и не переписываешь чужой стиль ради вкуса. ## Контекст - **Версия Next.js:** {{versiia_nextjs}} - **Рендеринг:** {{strategiia_renderinga}} (SSR, SSG, ISR, смешанная) - **Что делает приложение:** {{opisanie_prilozeniia}} - **Код на ревью:** {{kod_komponentov_ili_marsrutov}} ## Что проверить **1. Границы Server / Client Components** - Лишние 'use client' на компонентах без интерактивности и браузерных API - Несериализуемые пропсы через границу server → client - Серверная логика (запросы к БД, секреты), утекающая в клиентский бандл **2. Данные и кеширование** - fetch без осознанной стратегии (cache, next.revalidate) — случайный static/dynamic - Водопады последовательных запросов там, где нужен параллельный Promise.all - Мутации без revalidatePath/revalidateTag — устаревшие данные после записи **3. Маршрутизация и структура** - Неправильное использование layout / page / loading / error файлов - Динамические сегменты без generateStaticParams там, где список известен - Метаданные: generateMetadata для динамики, статический export metadata для остального **4. Производительность** - Тяжёлые зависимости в клиентском бандле, которым место на сервере - Отсутствие Suspense-границ вокруг медленных данных - Сырые <img> и @font-face вместо next/image и next/font ## Формат ответа 1. **Резюме** — 2–3 предложения: общее состояние кода и главная проблема. 2. **Замечания по приоритету** — Критично / Важно / Желательно; для каждого: место в коде, объяснение и правка «до/после». 3. **Что сделано хорошо** — 2–3 пункта, чтобы рабочие паттерны не потерялись при правках. Не выдумывай проблем ради объёма: если блок в порядке — так и скажи одной строкой.
Заполните переменные

Пример запуска

Сгенерировано Claude Opus 5.5, 24 сентября 2026 г.

Значения переменных

Версия next.js
14.2.5 (App Router)
Стратегия рендеринга
Смешанная: каталог товаров — ISR (revalidate 300с), карточка товара — SSG с generateStaticParams частично + fallback SSR для новых товаров, админка заказов — SSR (force-dynamic)
Описание приложения
«Kaktus Store» — интернет-магазин комнатных растений и кашпо для небольшой розничной сети (3 офлайн-точки в Санкт-Петербурге + доставка по РФ). Стек: Next.js 14 App Router, Prisma + PostgreSQL (Neon), NextAuth (email+пароль для админов), Tailwind CSS, Zod для валидации форм. Публичная часть: - /catalog — витрина товаров с фильтрами по категории (кактусы, суккуленты, кашпо), карточки подгружаются списком, обновляется раз в 5 минут. - /catalog/[slug] — страница товара: галерея, описание, блок отзывов покупателей (можно оставить отзыв без регистрации, только имя и текст), блок «похожие товары». - /cart, /checkout — оформление заказа, интеграция с ЮKassa для оплаты. Приватная часть (за NextAuth, роль admin): - /admin/orders — список заказов с фильтром по статусу (новый/оплачен/отправлен/доставлен), возможность сменить статус вручную. - /admin/orders/[id] — карточка заказа, история изменений, кнопка «пересчитать сумму доставки» дергает внешний API СДЭК. Известные проблемы, с которыми уже сталкивались: после смены статуса заказа в админке список на /admin/orders иногда показывает старые данные ещё несколько секунд (приходится руками жать F5); карточка товара на мобильных долго грузится из-за большого JS-бандла; при добавлении отзыва страница товара визуально не обновляется, пока не перезайти. Хотим ревью конкретных файлов ниже, чтобы понять корневые причины.
Код компонентов или маршрутов
// app/catalog/[slug]/page.tsx import { prisma } from '@/lib/prisma' import ProductGallery from './ProductGallery' import ReviewsBlock from './ReviewsBlock' import RelatedProducts from './RelatedProducts' export default async function ProductPage({ params }: { params: { slug: string } }) { const product = await prisma.product.findUnique({ where: { slug: params.slug }, include: { images: true }, }) const reviews = await prisma.review.findMany({ where: { productId: product?.id }, orderBy: { createdAt: 'desc' }, }) const related = await prisma.product.findMany({ where: { category: product?.category }, take: 4, }) return ( <div className="grid grid-cols-2 gap-8"> <ProductGallery images={product.images} /> <div> <h1 className="text-2xl font-bold">{product.name}</h1> <p>{product.description}</p> <ReviewsBlock reviews={reviews} db={prisma} productId={product.id} /> </div> <RelatedProducts items={related} /> </div> ) } // app/catalog/[slug]/ReviewsBlock.tsx 'use client' import { addReview } from './actions' export default function ReviewsBlock({ reviews, db, productId }: any) { return ( <div> <ul> {reviews.map((r: any) => ( <li key={r.id}> <b>{r.authorName}</b>: {r.text} </li> ))} </ul> <form action={addReview}> <input type="hidden" name="productId" value={productId} /> <input name="authorName" placeholder="Ваше имя" /> <textarea name="text" placeholder="Отзыв" /> <button type="submit">Отправить</button> </form> </div> ) } // app/catalog/[slug]/actions.ts 'use server' import { prisma } from '@/lib/prisma' export async function addReview(formData: FormData) { const productId = formData.get('productId') as string const authorName = formData.get('authorName') as string const text = formData.get('text') as string await prisma.review.create({ data: { productId, authorName, text }, }) } // app/catalog/[slug]/ProductGallery.tsx 'use client' export default function ProductGallery({ images }: { images: { url: string }[] }) { return ( <div className="flex gap-2"> {images.map((img) => ( <img key={img.url} src={img.url} alt="" width={300} /> ))} </div> ) } // app/admin/orders/page.tsx import { prisma } from '@/lib/prisma' import OrderStatusForm from './OrderStatusForm' export default async function OrdersPage() { const orders = await prisma.order.findMany({ orderBy: { createdAt: 'desc' }, }) return ( <table> <tbody> {orders.map((o) => ( <tr key={o.id}> <td>{o.id}</td> <td>{o.status}</td> <td> <OrderStatusForm orderId={o.id} currentStatus={o.status} /> </td> </tr> ))} </tbody> </table> ) } // app/admin/orders/OrderStatusForm.tsx 'use client' import { updateStatus } from './actions' export default function OrderStatusForm({ orderId, currentStatus }: { orderId: string; currentStatus: string }) { return ( <form action={updateStatus.bind(null, orderId)}> <select name="status" defaultValue={currentStatus}> <option value="new">Новый</option> <option value="paid">Оплачен</option> <option value="shipped">Отправлен</option> <option value="delivered">Доставлен</option> </select> <button type="submit">Сохранить</button> </form> ) } // app/admin/orders/actions.ts 'use server' import { prisma } from '@/lib/prisma' export async function updateStatus(orderId: string, formData: FormData) { const status = formData.get('status') as string await prisma.order.update({ where: { id: orderId }, data: { status }, }) }

Ревью Kaktus Store: страница товара и админка заказов

Резюме

Архитектура в целом правильная: данные загружаются в серверных компонентах, мутации сделаны через Server Actions, клиентские компоненты небольшие. Проблемы сосредоточены в мутациях. Ни один Server Action не проверяет, кто его вызывает и что пришло, и ни один не инвалидирует кеш. Отсутствие инвалидации — корневая причина обоих известных багов: устаревшего списка заказов и «невидимого» нового отзыва. Отсутствие проверки роли в updateStatus — дыра в безопасности, её нужно закрыть до всего остального.


Замечания по приоритету

🔴 Критично

1. updateStatus не проверяет авторизацию и значение статуса

Где: app/admin/orders/actions.ts

Почему это проблема. Server Action — это публичный POST-эндпоинт с ID действия. Проверка сессии в layout.tsx или на странице его не защищает: layout не выполняется при вызове action, а action можно вызвать напрямую, минуя UI. Документация Next.js прямо требует проверять авторизацию внутри каждого action.

Если /admin/* закрыт middleware, это частично спасает. Но полагаться на matcher как на единственную защиту хрупко: достаточно одного изменения конфигурации.

Ещё два момента:

  • Аргументы из .bind(null, orderId) в Next 14 не шифруются. Они приходят с клиента и могут быть подменены, поэтому orderId нельзя считать доверенным.
  • status — произвольная строка. В базу можно записать "hacked", а если в Prisma это enum, упадёт необработанное исключение.

До:

export async function updateStatus(orderId: string, formData: FormData) {
  const status = formData.get('status') as string
  await prisma.order.update({ where: { id: orderId }, data: { status } })
}

После:

'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'

const StatusSchema = z.enum(['new', 'paid', 'shipped', 'delivered'])

export async function updateStatus(orderId: string, formData: FormData) {
  const session = await getServerSession(authOptions)
  // поле role зависит от ваших callbacks в NextAuth
  if (session?.user?.role !== 'admin') {
    throw new Error('Forbidden')
  }

  const status = StatusSchema.parse(formData.get('status'))
  const id = z.string().min(1).parse(orderId)

  await prisma.order.update({ where: { id }, data: { status } })

  revalidatePath('/admin/orders')
  revalidatePath(`/admin/orders/${id}`)
}

Если в карточке заказа есть «история изменений», её запись логично делать здесь же, в одной prisma.$transaction с обновлением. В показанном коде история не пишется. Проверьте, что она не теряется при ручной смене статуса.


2. PrismaClient передаётся в Client Component

Где: page.tsx → <ReviewsBlock ... db={prisma} />

Почему это проблема. Через границу server → client можно передавать только сериализуемые значения: примитивы, простые объекты, массивы, Date, промисы и Server Actions. PrismaClient — экземпляр класса, и React выдаст ошибку «Only plain objects… can be passed to Client Components».

Даже если бы это работало, клиенту нечего делать с подключением к базе. Проп при этом нигде не используется. Проскочил он из-за any в типах ReviewsBlock (см. п. 14).

До:

<ReviewsBlock reviews={reviews} db={prisma} productId={product.id} />

После:

<ReviewsBlock reviews={reviews} productId={product.id} />

3. Нет обработки несуществующего товара: краш и выборка чужих данных

Где: page.tsx, всё после findUnique.

Почему это проблема. Если slug не найден, product === null, и происходит следующее:

  • where: { productId: product?.id } превращается в { productId: undefined }. Prisma трактует undefined как «фильтра нет», поэтому загружаются все отзывы всех товаров.
  • where: { category: undefined } возвращает 4 случайных товара.
  • product.images выбрасывает TypeError. Вместо 404 пользователь получает 500, а поисковик — ошибку сервера.

При dynamicParams = true (fallback для новых товаров) любой бот, перебирающий URL, будет генерировать тяжёлые запросы и ошибки.

До:

const product = await prisma.product.findUnique({ ... })
const reviews = await prisma.review.findMany({ where: { productId: product?.id }, ... })

После:

import { notFound } from 'next/navigation'

const product = await getProduct(params.slug)
if (!product) notFound()
// ниже product гарантированно не null, и TS это знает

Добавьте app/catalog/[slug]/not-found.tsx с дружелюбной страницей «Товар не найден» и ссылкой в каталог.


4. Корневая причина бага «список заказов показывает старые данные»

Где: admin/orders/actions.ts: нет revalidatePath.

Почему это проблема. Когда Server Action завершается вызовом revalidatePath, Next.js в том же ответе отдаёт свежий RSC-payload текущего маршрута и сбрасывает клиентский Router Cache. Без этого вызова происходит следующее:

  1. Запись в базу проходит, но клиент новый payload не получает. <select> показывает выбранное значение (он uncontrolled), а ячейка <td>{o.status}</td> остаётся старой.
  2. Если перейти на /admin/orders/[id] и вернуться, Router Cache отдаёт закешированную версию. В 14.2 она хранится 30 секунд для динамических страниц. Отсюда и «несколько секунд, потом F5».

Исправление уже включено в п. 1: revalidatePath('/admin/orders') и revalidatePath('/admin/orders/${id}').

Отдельно проверьте, что страница действительно динамическая. В показанном page.tsx нет export const dynamic = 'force-dynamic'. Prisma — это не fetch, поэтому Next сам динамику не обнаружит. Если layout админки не читает cookies()/headers() (например, через getServerSession), страница соберётся статически на этапе build и будет показывать снимок заказов с момента деплоя. Лучше объявить это явно:

// app/admin/orders/page.tsx (или app/admin/layout.tsx)
export const dynamic = 'force-dynamic'

5. Корневая причина бага «отзыв не появляется»

Где: catalog/[slug]/actions.ts: нет revalidatePath.

Почему это проблема. Страница товара статическая (SSG/ISR). HTML и RSC-payload лежат в Full Route Cache. addReview пишет в базу, но кеш страницы не трогает. Пользователь видит старую версию, пока не истечёт revalidate, а если его нет — вообще никогда (см. п. 7). «Перезайти» помогает только тогда, когда совпадёт с фоновой регенерацией.

Исправление объединено с п. 6 ниже: там же валидация, и slug для инвалидации берётся из базы.


🟠 Важно

6. addReview — анонимная запись без валидации и защиты от спама

Где: catalog/[slug]/actions.ts

Почему это проблема. Это единственный эндпоинт, в который пишет кто угодно без регистрации. Сейчас:

  • Пустое имя, пустой текст или текст на 5 МБ сохраняются без ограничений.
  • Несуществующий productId приводит к ошибке внешнего ключа, пользователь видит error boundary вместо сообщения.
  • Каждый вызов после исправления п. 5 запускает регенерацию ISR. Без rate limit это ещё и способ нагрузить сервер.

XSS здесь, кстати, нет: React экранирует {r.text}. Это хорошо, только не переходите на dangerouslySetInnerHTML.

После:

// app/catalog/[slug]/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { prisma } from '@/lib/prisma'

const ReviewSchema = z.object({
  productId: z.string().min(1),
  authorName: z.string().trim().min(1, 'Укажите имя').max(60),
  text: z.string().trim().min(3, 'Слишком короткий отзыв').max(2000),
})

export type ReviewState = {
  ok: boolean
  errors?: Record<string, string[] | undefined>
}

export async function addReview(
  _prev: ReviewState,
  formData: FormData,
): Promise<ReviewState> {
  const parsed = ReviewSchema.safeParse(Object.fromEntries(formData))
  if (!parsed.success) {
    return { ok: false, errors: parsed.error.flatten().fieldErrors }
  }

  // slug берём из БД, а не с клиента, — нельзя инвалидировать произвольный путь
  const product = await prisma.product.findUnique({
    where: { id: parsed.data.productId },
    select: { slug: true },
  })
  if (!product) return { ok: false, errors: { productId: ['Товар не найден'] } }

  // TODO: rate limit по IP (например, @upstash/ratelimit) + honeypot-поле
  await prisma.review.create({ data: parsed.data })

  revalidatePath(`/catalog/${product.slug}`)
  return { ok: true }
}

Премодерация (флаг approved) — продуктовое решение, не требование. Но для анонимных отзывов её стоит обсудить.


7. На странице товара нет конфигурации рендеринга, заявленной в контексте

Где: catalog/[slug]/page.tsx

Почему это проблема. По описанию это SSG с частичным generateStaticParams и fallback. В коде нет ни generateStaticParams, ни revalidate. Если их нет и в родительском layout, происходит следующее:

  • На этапе build ничего не пререндерится, каждый товар рендерится при первом запросе.
  • Отрендеренная страница кешируется бессрочно: запросы через Prisma не делают маршрут динамическим. Цена, наличие и описание не обновятся до следующего деплоя.

После:

export const revalidate = 300   // как у каталога; плюс on-demand через revalidatePath
export const dynamicParams = true // новые товары рендерятся по запросу (это и есть fallback)

export async function generateStaticParams() {
  const products = await prisma.product.findMany({
    select: { slug: true },
    take: 100, // например, топ по продажам/новизне
  })
  return products.map((p) => ({ slug: p.slug }))
}

Когда в админке появится редактирование товаров, добавьте туда revalidatePath('/catalog/' + slug) и revalidatePath('/catalog').


8. Водопад из трёх последовательных запросов

Где: page.tsx, три await подряд.

Почему это проблема. reviews и related зависят от product (id и category), но не друг от друга. Сейчас время ответа складывается из трёх сетевых кругов до Neon. У serverless-Postgres каждый круг заметен, особенно на холодном соединении. Параллельный запуск убирает один круг.

После (вместе с п. 3 и п. 11):

import { cache } from 'react'
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import { prisma } from '@/lib/prisma'

// cache() дедуплицирует запрос между generateMetadata и page в рамках одного рендера.
// Для fetch это делается автоматически, для Prisma — нет.
const getProduct = cache((slug: string) =>
  prisma.product.findUnique({ where: { slug }, include: { images: true } }),
)

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug)
  if (!product) notFound()

  const [reviews, related] = await Promise.all([
    prisma.review.findMany({
      where: { productId: product.id },
      orderBy: { createdAt: 'desc' },
      take: 50,
    }),
    prisma.product.findMany({
      where: { category: product.category, id: { not: product.id } },
      take: 4,
    }),
  ])

  return (/* ... */)
}

9. Медленная загрузка на мобильных: скорее всего, дело в картинках, а не в JS

Где: ProductGallery.tsx

Почему это проблема. Честно: показанные клиентские компоненты крошечные и не объясняют «большой JS-бандл». Реальная причина медленной карточки, скорее всего, в другом. Сырой <img width={300}> заставляет браузер качать оригинал (часто 2–4 МБ с камеры) без srcset, без WebP/AVIF и без lazy loading для нижних картинок. На мобильном интернете это и даёт медленный LCP.

next/image сжимает, отдаёт нужный размер под экран и формат, лениво грузит всё, кроме priority, и резервирует место (нет CLS).

До:

'use client'
export default function ProductGallery({ images }: { images: { url: string }[] }) {
  return (
    <div className="flex gap-2">
      {images.map((img) => <img key={img.url} src={img.url} alt="" width={300} />)}
    </div>
  )
}

После:

// без 'use client' — интерактивности нет
import Image from 'next/image'

type Props = { images: { url: string }[]; productName: string }

export default function ProductGallery({ images, productName }: Props) {
  return (
    <div className="flex gap-2">
      {images.map((img, i) => (
        <Image
          key={img.url}
          src={img.url}
          alt={`${productName} — фото ${i + 1}`}
          width={300}
          height={300}
          sizes="(max-width: 768px) 50vw, 300px"
          priority={i === 0} // первое фото — кандидат в LCP
          className="object-cover"
        />
      ))}
    </div>
  )
}

Для внешнего хранилища картинок добавьте images.remotePatterns в next.config.js. Пустой alt="" у фото товара вредит и доступности, и SEO картинок.

Чтобы найти, где на самом деле тяжёлый JS, посмотрите колонку First Load JS в выводе next build и прогоните @next/bundle-analyzer. Типичные подозреваемые, которых нет в показанных файлах:

  • RelatedProducts (не показан; если там 'use client' и слайдер, это кандидат №1);
  • SessionProvider из NextAuth в корневом layout: он тянет next-auth/react на все публичные страницы, хотя авторизация нужна только в /admin;
  • библиотеки каруселей и дат, импортированные целиком.

10. Лишний 'use client' у ReviewsBlock

Где: ReviewsBlock.tsx

Почему это проблема. <form action={serverAction}> работает и в серверном компоненте, причём с progressive enhancement: форма отправляется даже до гидратации. Сейчас весь список отзывов уходит в клиентский компонент: массив сериализуется в RSC-payload как пропсы, плюс JS компонента. Клиентская интерактивность нужна только форме, и то лишь если хотите показывать ошибки валидации и состояние отправки. Поэтому лучше разделить: список на сервере, форма — маленький клиентский остров.

После:

// ReviewsBlock.tsx — серверный компонент
import type { Review } from '@prisma/client'
import ReviewForm from './ReviewForm'

export default function ReviewsBlock({ reviews, productId }: { reviews: Review[]; productId: string }) {
  return (
    <section>
      <ul>
        {reviews.map((r) => (
          <li key={r.id}><b>{r.authorName}</b>: {r.text}</li>
        ))}
      </ul>
      <ReviewForm productId={productId} />
    </section>
  )
}
// ReviewForm.tsx — единственный клиентский кусок
'use client'
import { useEffect, useRef } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { addReview, type ReviewState } from './actions'

function SubmitButton() {
  const { pending } = useFormStatus()
  return <button type="submit" disabled={pending}>{pending ? 'Отправляем…' : 'Отправить'}</button>
}

export default function ReviewForm({ productId }: { productId: string }) {
  const [state, action] = useFormState<ReviewState, FormData>(addReview, { ok: false })
  const formRef = useRef<HTMLFormElement>(null)

  useEffect(() => { if (state.ok) formRef.current?.reset() }, [state])

  return (
    <form ref={formRef} action={action}>
      <input type="hidden" name="productId" value={productId} />
      <input name="authorName" placeholder="Ваше имя" />
      {state.errors?.authorName && <p className="text-red-600">{state.errors.authorName[0]}</p>}
      <textarea name="text" placeholder="Отзыв" />
      {state.errors?.text && <p className="text-red-600">{state.errors.text[0]}</p>}
      <SubmitButton />
    </form>
  )
}

Благодаря revalidatePath в action новый отзыв появится в списке сразу, в том же ответе, без перезахода.


11. Нет generateMetadata на странице товара

Где: catalog/[slug]/page.tsx

Почему это проблема. Для интернет-магазина карточка товара — главная посадочная страница из поиска. Сейчас у всех товаров одинаковый title из корневого layout, нет description и OG-картинки для превью в Telegram/VK.

После (использует getProduct из п. 8, второго запроса к базе не будет):

export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const product = await getProduct(params.slug)
  if (!product) return {}
  return {
    title: `${product.name} — Kaktus Store`,
    description: product.description?.slice(0, 160),
    openGraph: { images: product.images[0] ? [product.images[0].url] : [] },
  }
}

🟡 Желательно

12. «Похожие товары» включают сам товар

where: { category } без исключения текущего id показывает в блоке ту же позицию. Исправление уже есть в п. 8 (id: { not: product.id }). Если RelatedProducts показывает только название, цену и превью, добавьте select, чтобы не тянуть описания целиком.

13. Админка: пагинация, фильтр и обратная связь
  • prisma.order.findMany() без take и select со временем начнёт загружать тысячи заказов со всеми полями. Добавьте select нужных колонок и пагинацию. Фильтр по статусу из описания логично сделать через searchParams (?status=paid): URL станет shareable, а страница и так динамическая.
  • Кнопке «Сохранить» нужен useFormStatus (как SubmitButton в п. 10). Иначе админ жмёт повторно, не видя, что запрос идёт.
  • app/admin/orders/loading.tsx даст мгновенную обратную связь при навигации: страница динамическая и каждый раз ждёт базу.
14. any в пропсах ReviewsBlock

Именно any позволил проп db={prisma} пройти незамеченным (п. 2). Типы из @prisma/client (Review, Product) бесплатны и ловят такие ошибки на этапе компиляции.

15. Suspense-границы

Для статической страницы товара выигрыш от Suspense вокруг отзывов и похожих товаров небольшой: всё рендерится один раз при build или регенерации, а пользователь получает готовый HTML. Сейчас это не приоритет. Suspense станет полезен, если отзывы когда-нибудь станут динамическими (например, персонализированными). Для админки хватит loading.tsx из п. 13.

Шрифты: в показанных файлах их нет. Проверьте в корневом layout, что используется next/font, а не @font-face или <link> на Google Fonts.


Что сделано хорошо

  1. Мутации через <form action={serverAction}>. Это правильный идиоматичный паттерн App Router: нет ручных API-роутов, формы работают до гидратации. После правок выше останется только добавить проверки, архитектуру менять не нужно.
  2. updateStatus.bind(null, orderId) вместо hidden-поля. Чистый способ передать контекст в action. Стоит лишь помнить, что bound-аргументы приходят с клиента и валидируются как любой ввод.
  3. Правильное направление границ. Страницы — серверные компоненты, которые загружают данные напрямую из Prisma без промежуточного API. Клиентские компоненты — листья дерева, а не обёртки. Лишний 'use client' в двух местах легко снять, а не переделывать всю структуру.

Похожие промпты

Программирование и Разработка

Ревью pull request по diff

Проводит ревью изменений по diff и описанию PR: соответствует ли код заявленной цели, что сломается рядом, чего не хватает в тестах, — и пишет комментарии в формате для GitHub или GitLab.

{} 3 перем.
Программирование и Разработка

Ревью Python-кода: типизация и идиомы

Проводит ревью Python-кода: ошибки, которые проявятся в рантайме, аннотации типов, идиоматичность, обработка исключений и ресурсов — с правками «до/после».

{} 3 перем.
Программирование и Разработка

Ревью SQL-запросов и миграций

Проверяет SQL-запросы и миграции схемы: корректность на граничных данных, индексы под реальные выборки, блокировки при миграции на живой таблице и откат.

{} 3 перем.
Программирование и Разработка

SEO-аудит Next.js-приложения

Проверяет Next.js-приложение по SEO-чеклисту: метаданные и canonical, рендеринг для ботов, sitemap/robots, Core Web Vitals — и выдаёт план правок по приоритету.

{} 4 перем.