News & Insights (news.eswf.dev) — Кроки імплементації¶
Огляд¶
News — це блог/новинний сайт про IT в логістиці, DOP-системи та автоматизацію бізнесу. Технології: React 19 + TypeScript + Vite 7 + Tailwind CSS v4 + React Router 7. Дизайн: темна тема, консистентна з порталом (eswf.dev).
Поки backend не реалізовано — використовуються mock-дані. API-клієнт підготовлений для інтеграції з Django REST (/api/v1/news/).
Крок 1. Scaffold проекту¶
Vite створює базову структуру React + TypeScript проекту з конфігами tsconfig.json, tsconfig.app.json, tsconfig.node.json, vite.config.ts.
Залежності¶
# Runtime
npm install react-router-dom axios @tabler/icons-react
# Dev (Tailwind v4 + typography plugin)
npm install -D tailwindcss @tailwindcss/vite @tailwindcss/typography autoprefixer postcss
| Пакет | Призначення |
|---|---|
react-router-dom |
Клієнтська маршрутизація (SPA) |
axios |
HTTP-клієнт для API (готовий до підключення backend) |
@tabler/icons-react |
Іконки (як у portal) |
tailwindcss v4 |
Утилітарний CSS фреймворк |
@tailwindcss/vite |
Інтеграція Tailwind v4 через Vite plugin (замість PostCSS) |
@tailwindcss/typography |
Стилізація prose-контенту (тексти статей) |
Крок 2. Конфігурація¶
vite.config.ts — Vite конфіг¶
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()], // Tailwind v4 як Vite plugin
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // Path alias: @/ → ./src/
},
},
server: {
port: 5174, // Окремий порт (portal=3000, erp=5173, news=5174)
proxy: {
'/api': {
target: 'http://localhost:8000', // Django backend
changeOrigin: true,
},
},
},
})
Ключові рішення:
- Tailwind v4 через Vite plugin — швидше за PostCSS підхід, нативна інтеграція
- Path alias @/ — дозволяє писати import X from '@/components/X' замість відносних шляхів
- Proxy — в dev-режимі /api/* запити проксуються на Django (порт 8000)
tsconfig.app.json — TypeScript path aliases¶
Додано baseUrl та paths для підтримки @/ alias:
src/index.css — Tailwind v4 + шрифти¶
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: 'Inter', system-ui, sans-serif;
}
body {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Tailwind v4 відмінності від v3:
- @import "tailwindcss" замість @tailwind base/components/utilities
- @plugin замість plugins: [] в конфіг-файлі
- @theme {} замість tailwind.config.ts → theme.extend
- Конфіг-файл tailwind.config.ts не потрібен — все в CSS
index.html — точка входу¶
<html lang="uk">
<body class="bg-gray-950 text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Темна тема задається прямо на <body>: bg-gray-950 text-white.
Крок 3. Типи та mock-дані¶
src/types/index.ts — TypeScript інтерфейси¶
export interface Category {
id: number;
name: string; // "Технології"
slug: string; // "tech"
description: string;
color: string; // "blue" | "green" | "purple" | "amber" — для CategoryBadge
}
export interface Tag {
id: number;
name: string; // "AI"
slug: string; // "ai"
}
export interface Article {
id: number;
title: string;
slug: string; // URL-friendly ідентифікатор
excerpt: string; // Короткий опис для картки
content: string; // Повний текст (markdown-подібний)
coverImage: string; // URL зображення (поки порожній)
category: Category; // Зв'язок з категорією
tags: Tag[]; // Масив тегів
author: string;
publishedAt: string; // ISO date
readTime: number; // Час читання в хвилинах
}
Чому slug а не id для URL: SEO-friendly URL (/article/ai-logistics-2026 замість /article/1).
src/data/mock.ts — тестові дані¶
4 категорії, 8 тегів, 6 статей на тематику IT + логістика:
| # | Стаття | Категорія | Теги |
|---|---|---|---|
| 1 | Як AI змінює логістику у 2026 | Логістика | AI, Fleet |
| 2 | DOP для малого бізнесу: з чого почати | DOP | Python, Django |
| 3 | GPS-моніторинг: ROI для автопарку | Логістика | GPS, Fleet |
| 4 | Електронні ТТН в Україні | Логістика | eTTN, Fleet |
| 5 | React vs Angular: вибір для enterprise | Технології | React, TypeScript |
| 6 | Оптимізація маршрутів: алгоритми та практика | Аналітика | AI, Fleet |
Контент статей — реалістичний текст з markdown-заголовками (##, ###), списками (-), жирним текстом (**...**).
Крок 4. API-клієнт¶
src/api/client.ts¶
const api = axios.create({
baseURL: '/api/v1/news',
});
const USE_MOCK = true; // Перемикач: true = mock-дані, false = Django API
export async function getArticles(): Promise<Article[]> { ... }
export async function getArticle(slug: string): Promise<Article | undefined> { ... }
export async function getCategories(): Promise<Category[]> { ... }
export async function getArticlesByCategory(categorySlug: string): Promise<Article[]> { ... }
Архітектура: кожна функція перевіряє USE_MOCK флаг. Коли backend буде готовий — достатньо змінити USE_MOCK = false, і всі запити підуть на Django REST API:
GET /api/v1/news/articles/— список статейGET /api/v1/news/articles/{slug}/— одна статтяGET /api/v1/news/categories/— категоріїGET /api/v1/news/articles/?category={slug}— фільтр за категорією
Крок 5. Компоненти¶
src/components/Layout.tsx — каркас сторінки¶
Обгортка для всіх сторінок з Header, Footer та <Outlet /> для React Router.
Header:
- Логотип ESWF.NEWS (стиль як portal: синій ESWF + зелений NEWS)
- Навігація: Головна + посилання на категорії (приховані на мобільних)
- Кнопка "← eswf.dev" для повернення на портал
- sticky top-0 + backdrop-blur-md — залишається зверху при скролі
Footer: - Копірайт + афоризм (як у portal)
src/components/ArticleCard.tsx — картка статті¶
Дизайн-система з portal:
- rounded-2xl border border-white/10 bg-white/5 backdrop-blur-sm — скляний ефект
- hover:-translate-y-1 hover:border-blue-400/30 — підйом при hover
- Gradient line зверху (opacity 0 → 1 при hover)
- Cover placeholder з великим "ESWF" watermark
- Category badge в лівому нижньому куті cover
- Мета: автор (IconUser), час читання (IconClock), дата
src/components/CategoryBadge.tsx — badge категорії¶
Кольоровий pill з маппінгом color → Tailwind класи:
- blue → bg-blue-500/15 text-blue-400 border-blue-500/30
- green → bg-emerald-500/15 text-emerald-400 border-emerald-500/30
- purple → bg-purple-500/15 text-purple-400 border-purple-500/30
- amber → bg-amber-500/15 text-amber-400 border-amber-500/30
Клікабельний — переходить на сторінку категорії.
src/components/TagBadge.tsx — badge тегу¶
Простий label: bg-white/5 border-white/10, текст #AI, #React тощо.
src/components/SearchBar.tsx — пошук¶
Input з іконкою пошуку (IconSearch). Controlled компонент (value + onChange).
Стилі: border-white/10 bg-white/5, при фокусі focus:border-blue-500/50 focus:bg-white/10.
Крок 6. Сторінки¶
src/pages/Home.tsx — головна (стрічка статей)¶
- Hero секція — заголовок "News & Insights" з іконкою + підзаголовок
- SearchBar — пошук по заголовку, excerpt та тегах (
useMemoдля фільтрації) - Grid статей —
grid-cols-1 md:grid-cols-2 lg:grid-cols-3 - Empty state — "Статей не знайдено" коли пошук не дає результатів
src/pages/Article.tsx — сторінка статті¶
- "← Усі статті" — посилання назад
- Category badge + заголовок + excerpt
- Мета — автор, дата (повна, українською), час читання
- Контент — простий markdown-рендерер (## → h2, ### → h3, - → li, ... → bold)
- Теги — список TagBadge внизу
- Схожі статті — до 2 статей тієї ж категорії
Markdown-рендерер — спрощений, парсить рядок за рядком. Для production варто замінити на react-markdown або remark.
src/pages/Category.tsx — сторінка категорії¶
- "← Усі статті" — посилання назад
- Назва категорії + опис + кількість статей
- Grid статей — відфільтровані за
category.slug - Empty state — якщо в категорії немає статей
Крок 7. Маршрутизація (App.tsx)¶
<BrowserRouter>
<Routes>
<Route element={<Layout />}> {/* Обгортка з Header/Footer */}
<Route index element={<Home />} /> {/* / */}
<Route path="article/:slug" element={<Article />} /> {/* /article/ai-logistics-2026 */}
<Route path="category/:slug" element={<Category />} /> {/* /category/tech */}
</Route>
</Routes>
</BrowserRouter>
Nested routing: Layout рендерить <Outlet />, в який React Router підставляє активну сторінку. Header і Footer залишаються спільними для всіх маршрутів.
Крок 8. Збірка та перевірка¶
cd frontend/news
npm run build # tsc -b && vite build — компіляція TS + production bundle
npm run dev # http://localhost:5174 — dev server з HMR
Результат build:
- dist/index.html — 0.52 KB
- dist/assets/index-*.css — 37.77 KB (Tailwind CSS)
- dist/assets/index-*.js — 256.19 KB (React + Router + Icons + код)
Структура файлів¶
frontend/news/
├── index.html # HTML точка входу (dark theme на body)
├── package.json # Залежності та скрипти
├── vite.config.ts # Vite + Tailwind plugin + proxy + aliases
├── tsconfig.json # Посилання на app/node конфіги
├── tsconfig.app.json # TS конфіг з path aliases (@/)
├── tsconfig.node.json # TS конфіг для Vite/Node
├── eslint.config.js # ESLint (від Vite scaffold)
│
├── src/
│ ├── main.tsx # Entry point: ReactDOM.createRoot
│ ├── App.tsx # BrowserRouter + Routes
│ ├── index.css # Tailwind v4 + Inter font + typography
│ │
│ ├── types/
│ │ └── index.ts # Article, Category, Tag інтерфейси
│ │
│ ├── data/
│ │ └── mock.ts # 6 статей, 4 категорії, 8 тегів
│ │
│ ├── api/
│ │ └── client.ts # Axios + mock fallback (USE_MOCK flag)
│ │
│ ├── components/
│ │ ├── Layout.tsx # Header (ESWF.NEWS) + Footer + Outlet
│ │ ├── ArticleCard.tsx # Картка статті (hover animations)
│ │ ├── CategoryBadge.tsx # Кольоровий pill категорії
│ │ ├── TagBadge.tsx # Badge тегу (#AI, #React)
│ │ └── SearchBar.tsx # Input пошуку з іконкою
│ │
│ └── pages/
│ ├── Home.tsx # Стрічка статей + пошук
│ ├── Article.tsx # Повна стаття + related
│ └── Category.tsx # Статті за категорією
│
└── dist/ # Production build output
Дизайн-система (консистентність з Portal)¶
| Елемент | Значення |
|---|---|
| Фон | bg-gray-950 |
| Текст основний | text-white |
| Текст вторинний | text-gray-400 |
| Текст третинний | text-gray-500 / text-gray-600 |
| Акцент primary | text-blue-400 / text-blue-700 |
| Акцент secondary | text-green-800 |
| Бордери | border-white/10 |
| Hover бордер | border-blue-400/30 |
| Фон карток | bg-white/5 |
| Hover фон | bg-white/10 |
| Заокруглення | rounded-2xl (картки), rounded-xl (input) |
| Backdrop | backdrop-blur-sm (картки), backdrop-blur-md (header) |
| Шрифт | Inter (Google Fonts) |
| Іконки | @tabler/icons-react |
Крок 9. Динамічні URL між сайтами (Environment Variables)¶
Проблема¶
Проект складається з декількох окремих додатків, кожен на своєму порті:
| Сайт | Dev (localhost) | Production |
|---|---|---|
| Portal | http://localhost:3000 |
https://eswf.dev |
| News | http://localhost:5174 |
https://news.eswf.dev |
| Shop | http://localhost:5175 |
https://shop.eswf.dev |
| DOP | http://localhost:5173 |
https://erp.eswf.dev |
Якщо захардкодити production URL (https://news.eswf.dev), посилання не працюватимуть в dev-середовищі. Потрібен механізм, щоб URL автоматично змінювались залежно від оточення.
Рішення: .env.development / .env.production¶
Обидва фреймворки (Next.js та Vite) автоматично завантажують відповідний .env файл:
- npm run dev → читає .env.development
- npm run build → читає .env.production
Важливо: префікси змінних відрізняються:
- Next.js (portal): NEXT_PUBLIC_ — робить змінну доступною в клієнтському коді
- Vite (news): VITE_ — аналогічно, змінна потрапляє в import.meta.env
Portal (Next.js) — посилання на під-сайти¶
frontend/portal/.env.development:
NEXT_PUBLIC_NEWS_URL=http://localhost:5174
NEXT_PUBLIC_SHOP_URL=http://localhost:5175
NEXT_PUBLIC_DOP_URL=http://localhost:5173
frontend/portal/.env.production:
NEXT_PUBLIC_NEWS_URL=https://news.eswf.dev
NEXT_PUBLIC_SHOP_URL=https://shop.eswf.dev
NEXT_PUBLIC_DOP_URL=https://erp.eswf.dev
components/CardLinks.tsx — використання:
const cards = [
{
title: "News & Insights",
href: process.env.NEXT_PUBLIC_NEWS_URL || "https://news.eswf.dev",
// ...
},
{
title: "Solution Catalog",
href: process.env.NEXT_PUBLIC_SHOP_URL || "https://shop.eswf.dev",
// ...
},
{
title: "DOP Demo Lab",
href: process.env.NEXT_PUBLIC_DOP_URL || "https://erp.eswf.dev",
// ...
},
];
process.env.NEXT_PUBLIC_* — Next.js інлайнить значення на етапі збірки. Fallback || "https://..." гарантує роботу навіть якщо .env файл відсутній.
News (Vite) — посилання назад на портал¶
frontend/news/.env.development:
frontend/news/.env.production:
src/components/Layout.tsx — використання:
const PORTAL_URL = import.meta.env.VITE_PORTAL_URL || 'https://eswf.dev';
// В JSX:
<a href={PORTAL_URL}>
<IconArrowLeft size={14} />
eswf.dev
</a>
import.meta.env.VITE_* — Vite інлайнить значення під час збірки. Змінні без префіксу VITE_ ігноруються з міркувань безпеки (щоб випадково не зливати серверні секрети в клієнтський код).
Структура файлів¶
frontend/portal/
├── .env.development # Dev URL: localhost:5174, localhost:5175, localhost:5173
├── .env.production # Prod URL: news.eswf.dev, shop.eswf.dev, erp.eswf.dev
└── components/
└── CardLinks.tsx # process.env.NEXT_PUBLIC_NEWS_URL
frontend/news/
├── .env.development # Dev URL: localhost:3000 (portal)
├── .env.production # Prod URL: eswf.dev (portal)
└── src/components/
└── Layout.tsx # import.meta.env.VITE_PORTAL_URL
Перевірка¶
- Запустити portal:
cd frontend/portal && npm run dev→http://localhost:3000 - Запустити news:
cd frontend/news && npm run dev→http://localhost:5174 - На portal клікнути "News & Insights" → відкриється
localhost:5174 - На news клікнути "← eswf.dev" → повернення на
localhost:3000
Додавання нових сайтів¶
При додаванні нового під-сайту (наприклад, shop):
1. Додати NEXT_PUBLIC_SHOP_URL в обидва .env файли portal
2. Додати VITE_PORTAL_URL в .env файли нового сайту
3. За потреби додати cross-links між іншими під-сайтами
Наступні кроки¶
- Backend — створити Django app
news/з моделями Article, Category, Tag - API — реалізувати ендпоінти
/api/v1/news/articles/та/api/v1/news/categories/ - Переключити
USE_MOCK = falseвapi/client.ts - Зображення — додати cover images для статей
- Пагінація — для великої кількості статей
- SEO — розглянути міграцію на Next.js або додати SSR