sno-blog/CLAUDE.md
2026-02-02 22:53:36 +03:00

24 KiB
Raw Permalink Blame History

СО ИМИТ ВолГУ - Блог

Сайт-блог для Совета обучающихся Института математики и информационных технологий Волгоградского государственного университета.

Требования проекта

Основные требования

  • Сайт-блог для СО ИМИТ ВолГУ
  • Чёрно-жёлтая пиксельная стилистика (см. image.png)
  • Чистая FSD (Feature-Sliced Design) архитектура
  • Sanity CMS как self-hosted решение (embedded Studio)
  • Использование плагина frontend-design для создания UI

Стилистика (на основе референса image.png)

  • Цветовая схема: чёрный фон (#000000) + жёлтый/золотой (#FFD700)
  • 3D изометрические элементы: буквы "ИМИТ" в изометрии
  • Пиксельные границы и блоки: квадратные элементы с чёткими краями
  • Глитч-эффекты: анимированные искажения текста
  • Пиксельный шрифт: Press Start 2P для заголовков
  • Высококонтрастный дизайн: минимум цветов, максимум контраста

Технологический стек

Категория Технология
Frontend Next.js 15 (App Router) + TypeScript
CMS Sanity (embedded Studio в /studio)
Архитектура Feature-Sliced Design (FSD)
Стилизация Tailwind CSS + CSS Variables
Sanity интеграция next-sanity + @sanity/image-url
Package Manager pnpm
Контейнеризация Docker Compose

Docker

Запуск через Docker Compose

docker compose up -d          # Запуск в фоне
docker compose up             # Запуск с логами
docker compose down           # Остановка
docker compose build --no-cache  # Пересборка

docker-compose.yml

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_SANITY_PROJECT_ID=${NEXT_PUBLIC_SANITY_PROJECT_ID}
      - NEXT_PUBLIC_SANITY_DATASET=${NEXT_PUBLIC_SANITY_DATASET}
      - NEXT_PUBLIC_SANITY_API_VERSION=${NEXT_PUBLIC_SANITY_API_VERSION}
      - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
    env_file:
      - .env.local
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.next
    restart: unless-stopped

Dockerfile

FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM base AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["pnpm", "dev"]

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

FROM base AS production
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Доступ к сервисам

Сервис URL Описание
Frontend http://localhost:3000 Основной сайт
Sanity Studio http://localhost:3000/studio CMS редактор (embedded)

Команды

pnpm dev          # Запуск dev-сервера (Next.js + Sanity Studio)
pnpm build        # Production сборка
pnpm start        # Запуск production сервера
pnpm lint         # Линтинг кода

Структура проекта (FSD + Next.js App Router)

sno-blog/
├── src/
│   ├── app/                          # Next.js App Router (НЕ FSD слой)
│   │   ├── (site)/                   # Route group для основного сайта
│   │   │   ├── layout.tsx            # Layout с Header/Footer
│   │   │   ├── page.tsx              # Главная → HomePage
│   │   │   ├── posts/
│   │   │   │   ├── page.tsx          # Список постов
│   │   │   │   └── [slug]/page.tsx   # Страница поста
│   │   │   ├── categories/
│   │   │   │   └── [slug]/page.tsx   # Посты категории
│   │   │   ├── events/
│   │   │   │   ├── page.tsx          # Список событий
│   │   │   │   └── [slug]/page.tsx   # Страница события
│   │   │   ├── about/page.tsx        # О нас
│   │   │   └── contacts/page.tsx     # Контакты
│   │   ├── studio/[[...tool]]/       # Sanity Studio (embedded)
│   │   │   └── page.tsx
│   │   ├── api/                      # API routes (если нужны)
│   │   ├── layout.tsx                # Root layout
│   │   ├── globals.css               # Глобальные стили
│   │   └── not-found.tsx             # 404 страница
│   │
│   ├── pages-fsd/                    # FSD: Pages layer (композиция)
│   │   ├── home/
│   │   │   ├── ui/HomePage.tsx
│   │   │   └── index.ts
│   │   ├── post/
│   │   │   ├── ui/PostPage.tsx
│   │   │   └── index.ts
│   │   ├── posts-list/
│   │   │   ├── ui/PostsListPage.tsx
│   │   │   └── index.ts
│   │   ├── events/
│   │   │   ├── ui/EventsPage.tsx
│   │   │   └── index.ts
│   │   ├── about/
│   │   │   ├── ui/AboutPage.tsx
│   │   │   └── index.ts
│   │   └── contacts/
│   │       ├── ui/ContactsPage.tsx
│   │       └── index.ts
│   │
│   ├── widgets/                      # FSD: Widgets layer
│   │   ├── header/
│   │   │   ├── ui/Header.tsx
│   │   │   └── index.ts
│   │   ├── footer/
│   │   │   ├── ui/Footer.tsx
│   │   │   └── index.ts
│   │   ├── hero-section/
│   │   │   ├── ui/HeroSection.tsx
│   │   │   └── index.ts
│   │   ├── posts-grid/
│   │   │   ├── ui/PostsGrid.tsx
│   │   │   └── index.ts
│   │   └── events-timeline/
│   │       ├── ui/EventsTimeline.tsx
│   │       └── index.ts
│   │
│   ├── features/                     # FSD: Features layer
│   │   ├── category-filter/
│   │   │   ├── ui/CategoryFilter.tsx
│   │   │   └── index.ts
│   │   ├── search-posts/
│   │   │   ├── ui/SearchPosts.tsx
│   │   │   ├── model/useSearch.ts
│   │   │   └── index.ts
│   │   └── share-post/
│   │       ├── ui/SharePost.tsx
│   │       └── index.ts
│   │
│   ├── entities/                     # FSD: Entities layer
│   │   ├── post/
│   │   │   ├── ui/PostCard.tsx
│   │   │   ├── ui/PostContent.tsx
│   │   │   ├── model/types.ts
│   │   │   ├── api/queries.ts
│   │   │   └── index.ts
│   │   ├── author/
│   │   │   ├── ui/AuthorCard.tsx
│   │   │   ├── model/types.ts
│   │   │   └── index.ts
│   │   ├── category/
│   │   │   ├── ui/CategoryBadge.tsx
│   │   │   ├── model/types.ts
│   │   │   └── index.ts
│   │   └── event/
│   │       ├── ui/EventCard.tsx
│   │       ├── model/types.ts
│   │       ├── api/queries.ts
│   │       └── index.ts
│   │
│   └── shared/                       # FSD: Shared layer
│       ├── ui/                       # UI Kit компоненты
│       │   ├── button/
│       │   │   ├── Button.tsx
│       │   │   └── index.ts
│       │   ├── card/
│       │   │   ├── Card.tsx
│       │   │   └── index.ts
│       │   ├── glitch-text/
│       │   │   ├── GlitchText.tsx
│       │   │   └── index.ts
│       │   ├── pixel-border/
│       │   │   ├── PixelBorder.tsx
│       │   │   └── index.ts
│       │   ├── container/
│       │   │   ├── Container.tsx
│       │   │   └── index.ts
│       │   ├── pixel-logo/
│       │   │   ├── PixelLogo.tsx       # 3D изометрический логотип ИМИТ
│       │   │   └── index.ts
│       │   └── index.ts              # Public API
│       │
│       ├── lib/
│       │   └── sanity/
│       │       ├── client.ts         # Sanity client
│       │       ├── queries.ts        # GROQ queries
│       │       ├── image.ts          # Image URL builder
│       │       └── index.ts
│       │
│       ├── config/
│       │   ├── site.ts               # Конфигурация сайта
│       │   └── navigation.ts         # Навигация
│       │
│       └── styles/
│           ├── variables.css         # CSS переменные
│           └── animations.css        # Анимации (глитч и др.)
│
├── sanity/                           # Sanity конфигурация
│   ├── schemas/
│   │   ├── documents/
│   │   │   ├── post.ts
│   │   │   ├── author.ts
│   │   │   ├── category.ts
│   │   │   └── event.ts
│   │   ├── objects/
│   │   │   └── blockContent.ts       # Portable Text
│   │   └── index.ts                  # Экспорт всех схем
│   └── lib/
│       └── client.ts                 # Sanity client для Studio
│
├── public/
│   └── fonts/                        # Локальные шрифты (если нужны)
│
├── sanity.config.ts                  # Конфигурация Sanity Studio
├── sanity.cli.ts                     # Sanity CLI конфигурация
├── next.config.ts                    # Next.js конфигурация
├── tailwind.config.ts                # Tailwind конфигурация
├── tsconfig.json
├── package.json
├── .env.local                        # Переменные окружения (не в git)
├── .env.example                      # Пример переменных
└── CLAUDE.md                         # Этот файл

FSD правила импортов

Слои могут импортировать ТОЛЬКО из нижележащих слоёв:

app (Next.js) → pages-fsd → widgets → features → entities → shared

Запрещено:

  • Импорт из вышележащих слоёв
  • Кросс-импорт внутри одного слоя (slice → slice)
  • Импорт минуя public API (index.ts)

Разрешено:

  • widgets/headershared/ui, shared/config
  • features/searchentities/post, shared/lib
  • entities/postshared/ui, shared/lib/sanity

Sanity CMS

Переменные окружения (.env.local)

# Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_API_VERSION=2024-01-01
SANITY_API_TOKEN=your_token_for_preview

# Site
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Схемы документов

Post (sanity/schemas/documents/post.ts)

import { defineField, defineType } from 'sanity'

export const postType = defineType({
  name: 'post',
  title: 'Пост',
  type: 'document',
  fields: [
    defineField({ name: 'title', title: 'Заголовок', type: 'string', validation: (Rule) => Rule.required() }),
    defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title', maxLength: 96 } }),
    defineField({ name: 'excerpt', title: 'Краткое описание', type: 'text', rows: 3 }),
    defineField({ name: 'mainImage', title: 'Главное изображение', type: 'image', options: { hotspot: true } }),
    defineField({ name: 'author', title: 'Автор', type: 'reference', to: { type: 'author' } }),
    defineField({ name: 'categories', title: 'Категории', type: 'array', of: [{ type: 'reference', to: { type: 'category' } }] }),
    defineField({ name: 'publishedAt', title: 'Дата публикации', type: 'datetime' }),
    defineField({ name: 'body', title: 'Содержимое', type: 'blockContent' }),
  ],
  preview: {
    select: { title: 'title', author: 'author.name', media: 'mainImage' },
    prepare({ title, author, media }) {
      return { title, subtitle: author ? `by ${author}` : '', media }
    },
  },
})

Author (sanity/schemas/documents/author.ts)

export const authorType = defineType({
  name: 'author',
  title: 'Автор',
  type: 'document',
  fields: [
    defineField({ name: 'name', title: 'Имя', type: 'string', validation: (Rule) => Rule.required() }),
    defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'name' } }),
    defineField({ name: 'avatar', title: 'Аватар', type: 'image' }),
    defineField({ name: 'role', title: 'Должность', type: 'string' }),
    defineField({ name: 'bio', title: 'Биография', type: 'text' }),
  ],
})

Category (sanity/schemas/documents/category.ts)

export const categoryType = defineType({
  name: 'category',
  title: 'Категория',
  type: 'document',
  fields: [
    defineField({ name: 'title', title: 'Название', type: 'string', validation: (Rule) => Rule.required() }),
    defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }),
    defineField({ name: 'description', title: 'Описание', type: 'text' }),
    defineField({ name: 'color', title: 'Цвет', type: 'string', description: 'HEX цвет для бейджа' }),
  ],
})

Event (sanity/schemas/documents/event.ts)

export const eventType = defineType({
  name: 'event',
  title: 'Событие',
  type: 'document',
  fields: [
    defineField({ name: 'title', title: 'Название', type: 'string', validation: (Rule) => Rule.required() }),
    defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }),
    defineField({ name: 'eventType', title: 'Тип события', type: 'string', options: { list: ['meeting', 'workshop', 'conference', 'other'] } }),
    defineField({ name: 'date', title: 'Дата начала', type: 'datetime', validation: (Rule) => Rule.required() }),
    defineField({ name: 'endDate', title: 'Дата окончания', type: 'datetime' }),
    defineField({ name: 'location', title: 'Место', type: 'string' }),
    defineField({ name: 'image', title: 'Изображение', type: 'image' }),
    defineField({ name: 'description', title: 'Описание', type: 'blockContent' }),
    defineField({ name: 'isHighlighted', title: 'Выделить', type: 'boolean', initialValue: false }),
  ],
})

GROQ Queries (src/shared/lib/sanity/queries.ts)

import { defineQuery } from 'next-sanity'

// Все посты
export const POSTS_QUERY = defineQuery(`
  *[_type == "post"] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    mainImage,
    publishedAt,
    "author": author->{name, avatar},
    "categories": categories[]->{title, slug, color}
  }
`)

// Пост по slug
export const POST_BY_SLUG_QUERY = defineQuery(`
  *[_type == "post" && slug.current == $slug][0] {
    _id,
    title,
    slug,
    excerpt,
    mainImage,
    body,
    publishedAt,
    "author": author->{name, slug, avatar, bio, role},
    "categories": categories[]->{title, slug, color}
  }
`)

// Посты по категории
export const POSTS_BY_CATEGORY_QUERY = defineQuery(`
  *[_type == "post" && $categorySlug in categories[]->slug.current] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    mainImage,
    publishedAt,
    "author": author->{name, avatar},
    "categories": categories[]->{title, slug, color}
  }
`)

// Все события
export const EVENTS_QUERY = defineQuery(`
  *[_type == "event"] | order(date desc) {
    _id,
    title,
    slug,
    eventType,
    date,
    endDate,
    location,
    image,
    isHighlighted
  }
`)

// Все категории
export const CATEGORIES_QUERY = defineQuery(`
  *[_type == "category"] | order(title asc) {
    _id,
    title,
    slug,
    description,
    color
  }
`)

Sanity Client (src/shared/lib/sanity/client.ts)

import { createClient } from 'next-sanity'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-01-01',
  useCdn: process.env.NODE_ENV === 'production',
})

Embedded Studio (src/app/studio/...tool/page.tsx)

'use client'
import { NextStudio } from 'next-sanity/studio'
import config from '../../../../sanity.config'

export default function StudioPage() {
  return <NextStudio config={config} />
}

Дизайн-система

CSS переменные (src/shared/styles/variables.css)

:root {
  /* Цвета */
  --color-background: #000000;
  --color-surface: #0a0a0a;
  --color-primary: #FFD700;
  --color-primary-dark: #B8860B;
  --color-primary-light: #FFEC8B;
  --color-text: #FFFFFF;
  --color-text-muted: #888888;
  --color-border: #333333;
  --color-error: #FF4444;

  /* Шрифты */
  --font-pixel: 'Press Start 2P', monospace;
  --font-body: system-ui, -apple-system, sans-serif;

  /* Размеры */
  --pixel-size: 4px;
  --border-width: 4px;
  --container-max: 1200px;

  /* Тени */
  --shadow-pixel: 4px 4px 0 var(--color-primary);
  --shadow-pixel-hover: 6px 6px 0 var(--color-primary);

  /* Анимации */
  --transition-fast: 150ms ease;
  --transition-normal: 300ms ease;
}

Глитч-анимация (src/shared/styles/animations.css)

@keyframes glitch {
  0% { transform: translate(0); }
  20% { transform: translate(-2px, 2px); }
  40% { transform: translate(-2px, -2px); }
  60% { transform: translate(2px, 2px); }
  80% { transform: translate(2px, -2px); }
  100% { transform: translate(0); }
}

@keyframes glitch-skew {
  0% { transform: skew(0deg); }
  20% { transform: skew(2deg); }
  40% { transform: skew(-2deg); }
  60% { transform: skew(1deg); }
  80% { transform: skew(-1deg); }
  100% { transform: skew(0deg); }
}

.glitch-text {
  position: relative;
  animation: glitch-skew 1s infinite linear alternate-reverse;
}

.glitch-text::before,
.glitch-text::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.glitch-text::before {
  color: var(--color-primary);
  animation: glitch 0.3s infinite;
  clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
  transform: translate(-2px, -2px);
}

.glitch-text::after {
  color: var(--color-text);
  animation: glitch 0.3s infinite reverse;
  clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
  transform: translate(2px, 2px);
}

UI компоненты

При создании UI компонентов использовать плагин frontend-design:

/frontend-design

Компоненты должны следовать пиксельной стилистике:

  • Button: пиксельные границы, тень, hover-эффект
  • Card: чёрный фон, жёлтая граница, пиксельная тень
  • GlitchText: текст с глитч-анимацией
  • PixelBorder: декоративная пиксельная рамка
  • Container: центрированный контейнер с max-width
  • PixelLogo: 3D изометрический логотип "ИМИТ"

Этапы реализации

Этап 1: Инициализация проекта

  1. Создать Next.js 15 проект с TypeScript и Tailwind
  2. Инициализировать Sanity проект
  3. Установить зависимости (next-sanity, @sanity/image-url)
  4. Создать FSD структуру папок
  5. Настроить Tailwind с CSS переменными

Этап 2: Shared слой

  1. CSS переменные и анимации
  2. Подключить Press Start 2P шрифт
  3. UI компоненты (Button, Card, GlitchText, PixelBorder, Container)
  4. Sanity client и image builder
  5. Site config

Этап 3: Sanity Studio

  1. Создать схемы документов (post, author, category, event)
  2. Создать blockContent схему
  3. Настроить sanity.config.ts
  4. Создать embedded Studio page (/studio)
  5. Написать GROQ queries

Этап 4: Entities слой

  1. Post entity (PostCard, PostContent, types, queries)
  2. Author entity (AuthorCard, types)
  3. Category entity (CategoryBadge, types)
  4. Event entity (EventCard, types, queries)

Этап 5: Widgets слой

  1. Header с навигацией и логотипом
  2. Footer с контактами и ссылками
  3. HeroSection с глитч-эффектом
  4. PostsGrid для отображения постов
  5. EventsTimeline для событий

Этап 6: Features слой

  1. CategoryFilter для фильтрации постов
  2. SearchPosts с поиском по заголовкам
  3. SharePost для шаринга в соцсети

Этап 7: Pages и роутинг

  1. Главная страница (/)
  2. Список постов (/posts)
  3. Страница поста (/posts/[slug])
  4. Страницы категорий (/categories/[slug])
  5. События (/events, /events/[slug])
  6. О нас (/about)
  7. Контакты (/contacts)
  8. 404 страница

Этап 8: Финализация

  1. SEO метаданные (metadata API)
  2. Open Graph изображения
  3. Sitemap
  4. Оптимизация изображений
  5. Проверка производительности

Верификация

  1. pnpm dev - проверить запуск dev-сервера
  2. Открыть http://localhost:3000 - главная страница
  3. Открыть http://localhost:3000/studio - Sanity Studio
  4. Создать тестовый пост в Studio
  5. Проверить отображение поста на сайте
  6. Проверить адаптивность (mobile/tablet/desktop)
  7. Проверить глитч-эффекты и анимации
  8. pnpm build - проверить production сборку

Правила разработки

Перед использованием библиотек

ОБЯЗАТЕЛЬНО читать документацию через Context7:

mcp__context7__resolve-library-id → mcp__context7__query-docs

При создании UI

Использовать плагин frontend-design для качественных интерфейсов.

Код

  • TypeScript strict mode
  • Именование: PascalCase для компонентов, camelCase для функций
  • Файлы компонентов: PascalCase.tsx
  • Экспорт через index.ts (public API)

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