24 KiB
24 KiB
СО ИМИТ ВолГУ - Блог
Сайт-блог для Совета обучающихся Института математики и информационных технологий Волгоградского государственного университета.
Требования проекта
Основные требования
- Сайт-блог для СО ИМИТ ВолГУ
- Чёрно-жёлтая пиксельная стилистика (см. 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/header→shared/ui,shared/configfeatures/search→entities/post,shared/libentities/post→shared/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: Инициализация проекта
- Создать Next.js 15 проект с TypeScript и Tailwind
- Инициализировать Sanity проект
- Установить зависимости (next-sanity, @sanity/image-url)
- Создать FSD структуру папок
- Настроить Tailwind с CSS переменными
Этап 2: Shared слой
- CSS переменные и анимации
- Подключить Press Start 2P шрифт
- UI компоненты (Button, Card, GlitchText, PixelBorder, Container)
- Sanity client и image builder
- Site config
Этап 3: Sanity Studio
- Создать схемы документов (post, author, category, event)
- Создать blockContent схему
- Настроить sanity.config.ts
- Создать embedded Studio page (/studio)
- Написать GROQ queries
Этап 4: Entities слой
- Post entity (PostCard, PostContent, types, queries)
- Author entity (AuthorCard, types)
- Category entity (CategoryBadge, types)
- Event entity (EventCard, types, queries)
Этап 5: Widgets слой
- Header с навигацией и логотипом
- Footer с контактами и ссылками
- HeroSection с глитч-эффектом
- PostsGrid для отображения постов
- EventsTimeline для событий
Этап 6: Features слой
- CategoryFilter для фильтрации постов
- SearchPosts с поиском по заголовкам
- SharePost для шаринга в соцсети
Этап 7: Pages и роутинг
- Главная страница (/)
- Список постов (/posts)
- Страница поста (/posts/[slug])
- Страницы категорий (/categories/[slug])
- События (/events, /events/[slug])
- О нас (/about)
- Контакты (/contacts)
- 404 страница
Этап 8: Финализация
- SEO метаданные (metadata API)
- Open Graph изображения
- Sitemap
- Оптимизация изображений
- Проверка производительности
Верификация
pnpm dev- проверить запуск dev-сервера- Открыть http://localhost:3000 - главная страница
- Открыть http://localhost:3000/studio - Sanity Studio
- Создать тестовый пост в Studio
- Проверить отображение поста на сайте
- Проверить адаптивность (mobile/tablet/desktop)
- Проверить глитч-эффекты и анимации
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)