Refactor SSR implementation and update documentation. Replaced Static Site Generation (SSG) with Server-Side Rendering (SSR) using react-dom/server. Updated scripts for SSR rendering and removed deprecated prerender-multi.js. Enhanced terms.html generation from React components. Updated dependencies for Babel and added support for canvas in SSR.
All checks were successful
platform/bro-js/bro.landing/pipeline/head This commit looks good

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-10-24 11:34:04 +03:00
parent 9110e79d6b
commit 08ffd5a740
6 changed files with 765 additions and 347 deletions

144
cloud.md
View File

@ -19,11 +19,12 @@
### Особенности:
**React 18** с TypeScript
**Static Site Generation** - HTML вкомпилирован для SEO
**Server-Side Rendering (SSR)** - полноценный рендеринг React компонентов для SEO
**Chakra UI** - современный UI framework
**i18next** - мультиязычность (ru/en)
**React Router** - клиентский роутинг
**Hydration** - гидратация статического контента
**Hydration** - гидратация SSR контента
**jsdom + canvas** - полноценная DOM эмуляция для SSR
### Страницы:
@ -46,8 +47,7 @@ bro.landing/
│ ├── index.tsx # Entry point + hydration
│ └── index.ejs # HTML шаблон
├── scripts/
│ ├── prerender-multi.js # ⭐ Основной SSG скрипт
│ └── ssr-prerender.js # Альтернативный SSR
│ └── ssr-render.js # ⭐ SSR скрипт (react-dom/server)
├── dist/ # Сборка (генерируется)
│ ├── index.html # Главная (SSG)
│ ├── terms.html # Соглашение (SSG)
@ -58,23 +58,25 @@ bro.landing/
---
## Static Site Generation (SSG)
## Server-Side Rendering (SSR)
### Как работает?
1. **Webpack** собирает React → `dist/index.js`
2. **Скрипт `prerender-multi.js`** автоматически:
- Читает `dist/index.html` (пустой шаблон)
- Вставляет статический контент в `<div id="app"></div>`
- Генерирует `dist/terms.html` из `terma.md`
3. **React hydration** при загрузке оживляет статический HTML
1. **Webpack** собирает React → `dist/index.js` + `dist/index.html` (шаблон)
2. **Скрипт `ssr-render.js`** автоматически:
- Настраивает окружение Node.js (jsdom + canvas)
- Импортирует React компоненты из `src/pages/`
- Рендерит компоненты через `react-dom/server`
- Инжектит HTML в `dist/index.html` и `dist/terms.html`
3. **React hydration** при загрузке оживляет SSR HTML
### Преимущества SSG:
### Преимущества SSR:
- 🚀 **Быстрая загрузка** - контент виден до загрузки JS
- 🔍 **SEO** - поисковики индексируют полный HTML
- 🔍 **SEO** - поисковики индексируют полный HTML с Chakra UI
- ♿ **Доступность** - работает без JavaScript
- 📊 **Метрики** - улучшенные FCP и LCP
- ✅ **Никакого хардкода** - рендер реальных компонентов
### Hydration в index.tsx:
@ -82,49 +84,37 @@ bro.landing/
const hasPrerenderedContent = MOUNT_NODE.hasChildNodes();
if (hasPrerenderedContent) {
hydrateRoot(MOUNT_NODE, <App />); // Оживляем статику
hydrateRoot(MOUNT_NODE, <App />); // Оживляем SSR HTML
} else {
createRoot(MOUNT_NODE).render(<App />); // Обычный рендер
}
```
### Скрипт prerender-multi.js:
### Скрипт ssr-render.js:
**Что делает**:
1. Читает `terma.md` (исходник соглашения)
2. Парсит markdown → HTML со стилями
3. Создает `dist/index.html` с контентом главной
4. Создает `dist/terms.html` с полным соглашением (~13KB)
1. Настраивает Babel для транспиляции TSX → JS
2. Настраивает jsdom для DOM эмуляции
3. Настраивает canvas для Lottie анимаций
4. Импортирует компоненты `UnderConstruction` и `Terms`
5. Рендерит через `renderToString(React.createElement(Component))`
6. Создает `dist/index.html` и `dist/terms.html` с полным HTML
**Автоматический запуск**:
- `npm run build:prod` - после webpack сборки
- `npm run build:prod:ssr` - альтернативный SSR
---
## Страница пользовательского соглашения
### Источник: `terma.md`
### Источник: `Terms.tsx`
⚠️ **Важно**: `terma.md` - единственный источник правды для соглашения!
⚠️ **Важно**: `src/pages/terms/Terms.tsx` - единственный источник правды для соглашения!
Чтобы обновить соглашение:
1. Отредактируйте `terma.md`
1. Отредактируйте `src/pages/terms/Terms.tsx`
2. Запустите `npm run build:prod`
3. `dist/terms.html` обновится автоматически
### Структура terma.md:
```markdown
Пользовательское соглашение для BROJS.RU
Последнее обновление: 25 мая 2025 г.
1. Термины
...
10. Контакты
Для обращений: primakov.pro@yandex.ru
```
3. `dist/terms.html` обновится автоматически через SSR
### React компонент: Terms.tsx
@ -132,21 +122,46 @@ if (hasPrerenderedContent) {
- **Дизайн**: Chakra UI, официальный стиль документа
- **SEO**: Helmet с meta-тегами
- **Роут**: `/terms` в dashboard.tsx
- **SSR**: Рендерится через `renderToString()` в `ssr-render.js`
### Структура компонента:
```tsx
export const Terms = () => {
return (
<>
<Helmet>
<title>Пользовательское соглашение - BROJS.RU</title>
<meta name="description" content="..." />
</Helmet>
<Box bg="gray.50" minH="100vh" py={8}>
<Container maxW="4xl" bg="white" shadow="lg" borderRadius="md" p={{ base: 6, md: 10 }}>
<VStack spacing={6} align="stretch">
<Heading as="h1">Пользовательское соглашение</Heading>
{/* ... полный контент ... */}
</VStack>
</Container>
</Box>
</>
);
};
```
### SEO-версия: terms.html
Генерируется автоматически из `terma.md`:
Генерируется автоматически через SSR из компонента `Terms.tsx`:
```html
<title>Пользовательское соглашение - BROJS.RU</title>
<meta name="description" content="Полное пользовательское соглашение..." />
<div id="app">
<div style="...красивые стили...">
<h1>Пользовательское соглашение для BROJS.RU</h1>
<h2>1. Термины</h2>
<ul>
<li>Платформа — сайт https://brojs.ru ...</li>
<div class="css-15jkess">
<div class="chakra-container css-ilwlhp">
<h1 class="chakra-heading css-s2s61i">Пользовательское соглашение</h1>
<p class="chakra-text css-130t7v7">для BROJS.RU</p>
<p class="chakra-text css-1pbebyg">Последнее обновление: 25 мая 2025 г.</p>
...
</ul>
<!-- Все 10 разделов -->
@ -174,8 +189,7 @@ if (hasPrerenderedContent) {
{
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build && node scripts/prerender-multi.js",
"build:prod:ssr": "npm run clean && brojs build && node scripts/ssr-prerender.js",
"build:prod": "npm run clean && brojs build && node scripts/ssr-render.js",
"clean": "rimraf dist",
"eslint": "npx eslint src",
"prettier": "prettier --write .",
@ -204,35 +218,30 @@ npm run build
# → Результат: dist/index.html (пустой шаблон)
```
#### 🚀 Production сборка (с SSG):
#### 🚀 Production сборка (с SSR):
```bash
npm run build:prod
# → Webpack в production режиме
# → Минификация
# → Автоматический SSG (prerender-multi.js)
# → Автоматический SSR (ssr-render.js)
# → Рендер реальных React компонентов через react-dom/server
# → Результат:
# - dist/index.html (со статическим контентом)
# - dist/terms.html (полное соглашение для SEO)
# - dist/index.html (UnderConstruction компонент)
# - dist/terms.html (Terms компонент с Chakra UI)
```
Вывод:
```
✅ Сборка успешно завершена!
🚀 Начинаем мульти-страничный пре-рендеринг...
✅ index.html обновлен
📝 Генерируем полный HTML из terma.md...
✅ terms.html создан с полным контентом
🎉 Пре-рендеринг завершен успешно!
```
#### 🔄 Production сборка (альтернативный SSR):
```bash
npm run build:prod:ssr
# → Использует ssr-prerender.js
# → SSR с jsdom окружением
# → Результат аналогичен build:prod
🚀 Запуск SSR с рендерингом React компонентов...
✅ Компоненты загружены
✅ Компоненты отрендерены
✅ index.html обновлен с SSR контентом
✅ terms.html создан с SSR контентом
🎉 SSR завершен успешно!
📄 Созданы: index.html, terms.html
💡 Весь контент отрендерен через React SSR
```
### Другие команды:
@ -267,12 +276,15 @@ npm run test # Запустить тесты
- **@brojs/cli 1.9.5** - сборщик (обертка над webpack)
- **Webpack 5** - бандлер
- **Babel** - транспиляция
- **jsdom** - SSR окружение
- **@babel/register** - транспиляция TSX в Node.js
- **@babel/preset-react** - JSX поддержка
- **@babel/preset-typescript** - TypeScript поддержка
- **jsdom 25.x** - DOM эмуляция для SSR
- **canvas 3.x** - Canvas API для Lottie в SSR
### Дополнительно:
- **Lottie React** - анимации
- **Lottie React** - анимации (работает в SSR!)
- **Day.js** - работа с датами
- **ESLint** - линтинг
- **Prettier** - форматирование

619
package-lock.json generated
View File

@ -33,9 +33,14 @@
"redux": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@babel/preset-react": "^7.28.5",
"@babel/register": "^7.28.3",
"@eslint/js": "^9.9.0",
"@types/jest": "^29.5.12",
"babel-jest": "^29.7.0",
"canvas": "^3.2.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
@ -48,19 +53,6 @@
"typescript-eslint": "^8.1.0"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
@ -161,21 +153,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.25.0",
"@babel/helper-compilation-targets": "^7.25.2",
"@babel/helper-module-transforms": "^7.25.2",
"@babel/helpers": "^7.25.0",
"@babel/parser": "^7.25.0",
"@babel/template": "^7.25.0",
"@babel/traverse": "^7.25.2",
"@babel/types": "^7.25.2",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -450,13 +442,13 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.0",
"@babel/types": "^7.25.6"
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@ -1861,6 +1853,165 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/register": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/register/-/register-7.28.3.tgz",
"integrity": "sha512-CieDOtd8u208eI49bYl4z1J22ySFw87IGwE+IswFEExH7e3rLgKb0WNQeumnacQ1+VoDJLYI5QFA3AJZuyZQfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"clone-deep": "^4.0.1",
"find-cache-dir": "^2.0.0",
"make-dir": "^2.1.0",
"pirates": "^4.0.6",
"source-map-support": "^0.5.16"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/register/node_modules/find-cache-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
"integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commondir": "^1.0.1",
"make-dir": "^2.0.0",
"pkg-dir": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/register/node_modules/p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/register/node_modules/pkg-dir": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
"integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"find-up": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/register/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/@babel/register/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@babel/register/node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@ -4267,6 +4418,16 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -5906,6 +6067,27 @@
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
@ -5947,6 +6129,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -6071,6 +6265,31 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@ -6165,6 +6384,21 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvas": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz",
"integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -6227,6 +6461,13 @@
"node": ">= 6"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@ -6415,6 +6656,13 @@
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
"license": "ISC"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/compute-scroll-into-view": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz",
@ -6911,6 +7159,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dedent": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@ -6926,6 +7190,16 @@
}
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -7072,6 +7346,16 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -8024,6 +8308,16 @@
"node": ">= 0.8.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
@ -8743,6 +9037,13 @@
"node": ">= 0.6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@ -8925,6 +9226,13 @@
"node": ">= 14"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT"
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -9533,6 +9841,27 @@
"postcss": "^8.1.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -11485,6 +11814,19 @@
"node": ">=6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -11522,6 +11864,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -11546,6 +11895,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -11589,6 +11945,39 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-abi": {
"version": "3.78.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
"integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-abi/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -12386,6 +12775,63 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/prebuild-install/node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -12742,6 +13188,32 @@
"node": ">= 0.8"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -13115,6 +13587,21 @@
}
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -13743,6 +14230,53 @@
"dev": true,
"license": "ISC"
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -13889,6 +14423,16 @@
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -14561,6 +15105,19 @@
"integrity": "sha512-+bGy9iDAqg3WSfc2ZrprToSPJhZjqy7vUv9wupQzsiv+BVPVx1T2a6G4T0290SpQj+56Toaw9BiLO5j5Bd7QzA==",
"license": "MIT"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -13,8 +13,7 @@
"test": "jest --coverage",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build && node scripts/prerender-multi.js",
"build:prod:ssr": "npm run clean && brojs build && node scripts/ssr-prerender.js"
"build:prod": "npm run clean && brojs build && node scripts/ssr-render.js"
},
"keywords": [],
"author": "",
@ -44,9 +43,14 @@
"redux": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@babel/preset-react": "^7.28.5",
"@babel/register": "^7.28.3",
"@eslint/js": "^9.9.0",
"@types/jest": "^29.5.12",
"babel-jest": "^29.7.0",
"canvas": "^3.2.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",

View File

@ -1,169 +0,0 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-undef */
const fs = require('fs');
const path = require('path');
// Контент для главной страницы
const homeContent = `
<div>
<div style="max-height: 250px;">
<div>Loading...</div>
</div>
<h3><center>Сайт в разработке</center></h3>
</div>
`.trim();
// Функция для генерации полного HTML из terma.md
const generateTermsContent = () => {
const termaPath = path.resolve(__dirname, '../terma.md');
const termaText = fs.readFileSync(termaPath, 'utf-8');
// Парсим markdown в HTML с сохранением структуры
let html = '<div style="background: #f7fafc; min-height: 100vh; padding: 32px 0;">';
html += '<div style="max-width: 1200px; margin: 0 auto; background: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); border-radius: 8px; padding: 60px 80px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;">';
const lines = termaText.split('\n');
let inList = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
if (inList) {
html += '</ul>';
inList = false;
}
continue;
}
// Заголовок H1
if (i === 0) {
html += '<div style="text-align: center; margin-bottom: 40px;">';
html += `<h1 style="font-size: 2.5em; color: #2563eb; margin-bottom: 8px; font-weight: 700;">${line}</h1>`;
continue;
}
// Дата обновления
if (line.includes('Последнее обновление')) {
html += `<p style="color: #6b7280; font-size: 0.875em;">${line}</p>`;
html += '</div><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 24px 0;">';
continue;
}
// Основные разделы (начинаются с цифры и точки без подразделов)
if (/^\d+\.\s+[А-Яа-я]/.test(line)) {
if (inList) {
html += '</ul>';
inList = false;
}
const text = line.replace(/^\d+\.\s+/, '');
html += `<h2 style="font-size: 1.875em; color: #1e40af; margin-top: 40px; margin-bottom: 16px; font-weight: 600;">${line}</h2>`;
continue;
}
// Подразделы (например, 2.1., 3.2.)
if (/^\d+\.\d+\.\s+/.test(line)) {
if (inList) {
html += '</ul>';
inList = false;
}
html += `<h3 style="font-size: 1.25em; color: #374151; margin-top: 24px; margin-bottom: 12px; font-weight: 600;">${line}</h3>`;
continue;
}
// Списки (начинаются с заглавной буквы или содержат "—")
if (line.includes('—') || (i > 0 && lines[i-1].includes('через:')) || (i > 0 && lines[i-1].includes('обязуется:')) || (i > 0 && lines[i-1].includes('собирает:')) || (i > 0 && lines[i-1].includes('ответственности за:'))) {
if (!inList) {
html += '<ul style="list-style-type: disc; padding-left: 32px; margin: 12px 0;">';
inList = true;
}
// Обработка ссылок
let processedLine = line
.replace(/https?:\/\/[^\s,)]+/g, (url) => {
const cleanUrl = url.replace(/\s*,?\s*$/, '');
return `<a href="${cleanUrl}" style="color: #3b82f6; text-decoration: underline;">${cleanUrl}</a>`;
})
.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
'<a href="mailto:$1" style="color: #3b82f6; text-decoration: underline;">$1</a>');
html += `<li style="margin: 8px 0; line-height: 1.75;">${processedLine}</li>`;
continue;
}
// Обычные параграфы
if (inList && !line.includes('—')) {
html += '</ul>';
inList = false;
}
// Обработка жирного текста и ссылок
let processedLine = line
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/https?:\/\/[^\s,)]+/g, (url) => {
const cleanUrl = url.replace(/\s*,?\s*$/, '');
return `<a href="${cleanUrl}" style="color: #3b82f6; text-decoration: underline;">${cleanUrl}</a>`;
})
.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
'<a href="mailto:$1" style="color: #3b82f6; text-decoration: underline;">$1</a>');
html += `<p style="margin: 12px 0; line-height: 1.75; color: #374151;">${processedLine}</p>`;
}
if (inList) {
html += '</ul>';
}
// Footer
html += '<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 48px 0 24px 0;">';
html += '<p style="text-align: center; color: #6b7280; font-size: 0.875em;">© 2025 BROJS.RU. Все права защищены.</p>';
html += '</div></div>';
return html;
};
const prerender = () => {
try {
console.log('🚀 Начинаем мульти-страничный пре-рендеринг...');
const distPath = path.resolve(__dirname, '../dist');
const indexPath = path.join(distPath, 'index.html');
// Читаем основной HTML
let indexHtml = fs.readFileSync(indexPath, 'utf-8');
// 1. Обрабатываем главную страницу
const searchString = '<div id="app"></div>';
if (indexHtml.includes(searchString)) {
indexHtml = indexHtml.replace(searchString, `<div id="app">${homeContent}</div>`);
fs.writeFileSync(indexPath, indexHtml, 'utf-8');
console.log('✅ index.html обновлен');
}
// 2. Генерируем полный контент для terms.html из terma.md
console.log('📝 Генерируем полный HTML из terma.md...');
const termsContent = generateTermsContent();
// 3. Создаем terms.html на основе index.html
let termsHtml = indexHtml
.replace(homeContent, termsContent)
.replace('<title>bro-js admin</title>', '<title>Пользовательское соглашение - BROJS.RU</title>')
.replace(
'</head>',
'<meta name="description" content="Полное пользовательское соглашение для платформы обучения фронтенд-разработке BROJS.RU. Условия использования, обработка персональных данных, права и обязанности сторон." /></head>'
);
const termsPath = path.join(distPath, 'terms.html');
fs.writeFileSync(termsPath, termsHtml, 'utf-8');
console.log('✅ terms.html создан с полным контентом');
console.log('🎉 Пре-рендеринг завершен успешно!');
console.log('📄 Созданы файлы: index.html, terms.html');
console.log('💡 terms.html содержит полный текст соглашения для SEO');
} catch (error) {
console.error('❌ Ошибка при пре-рендеринге:', error);
process.exit(1);
}
};
prerender();

View File

@ -1,79 +0,0 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-unused-vars */
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');
// Настройка окружения для SSR
const setupDOM = () => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost',
pretendToBeVisual: true,
resources: 'usable'
});
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
global.HTMLElement = dom.window.HTMLElement;
global.HTMLDivElement = dom.window.HTMLDivElement;
global.requestAnimationFrame = (callback) => setTimeout(callback, 0);
global.cancelAnimationFrame = clearTimeout;
};
const cleanupRender = () => {
delete global.window;
delete global.document;
delete global.navigator;
delete global.HTMLElement;
delete global.HTMLDivElement;
delete global.requestAnimationFrame;
delete global.cancelAnimationFrame;
};
const prerender = async () => {
try {
console.log('🚀 Начинаем SSR пре-рендеринг...');
setupDOM();
// Читаем HTML шаблон
const indexPath = path.resolve(__dirname, '../dist/index.html');
let html = fs.readFileSync(indexPath, 'utf-8');
// Рендерим статический контент страницы "в разработке"
const prerenderContent = `
<div style="text-align: center; padding: 20px;">
<div style="max-height: 250px; margin: 0 auto;">
<div></div>
</div>
<h3><center>Сайт в разработке</center></h3>
<p style="color: #666;">Страница загружается...</p>
</div>
`.trim();
// Вставляем пре-рендеренный контент в div#app
const searchString = '<div id="app"></div>';
if (html.includes(searchString)) {
html = html.replace(searchString, `<div id="app">${prerenderContent}</div>`);
// Сохраняем результат
fs.writeFileSync(indexPath, html, 'utf-8');
console.log('✅ SSR пре-рендеринг завершен успешно!');
console.log('📄 HTML обновлен с серверным контентом');
} else {
console.log('⚠️ Не найден <div id="app"></div>');
console.log('Возможно, HTML уже содержит пре-рендеренный контент');
}
cleanupRender();
} catch (error) {
console.error('❌ Ошибка при SSR пре-рендеринге:', error);
cleanupRender();
process.exit(1);
}
};
prerender();

93
scripts/ssr-render.js Normal file
View File

@ -0,0 +1,93 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-undef */
// Настройка Babel для транспиляции TSX/JSX в Node.js
require('@babel/register')({
extensions: ['.ts', '.tsx', '.js', '.jsx'],
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
ignore: [/node_modules/],
cache: false
});
const fs = require('fs');
const path = require('path');
const React = require('react');
const { renderToString } = require('react-dom/server');
const { JSDOM } = require('jsdom');
const { createCanvas } = require('canvas');
// Настройка полноценного DOM окружения через jsdom
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost',
pretendToBeVisual: true,
resources: 'usable'
});
const canvas = createCanvas(200, 200);
// Расширяем jsdom canvas поддержкой
dom.window.HTMLCanvasElement.prototype.getContext = function() {
return createCanvas(200, 200).getContext('2d');
};
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
global.HTMLElement = dom.window.HTMLElement;
global.SVGElement = dom.window.SVGElement;
console.log('🚀 Запуск SSR с рендерингом React компонентов...');
try {
// Импортируем компоненты
const { UnderConstruction } = require('../src/pages/under-construction/underConstruction.tsx');
const { Terms } = require('../src/pages/terms/Terms.tsx');
console.log('✅ Компоненты загружены');
// Рендерим компоненты в HTML
const homeContent = renderToString(React.createElement(UnderConstruction));
const termsContent = renderToString(React.createElement(Terms));
console.log('✅ Компоненты отрендерены');
// Читаем dist/index.html
const distPath = path.resolve(__dirname, '../dist');
const indexPath = path.join(distPath, 'index.html');
let indexHtml = fs.readFileSync(indexPath, 'utf-8');
// 1. Главная страница
const searchString = '<div id="app"></div>';
if (indexHtml.includes(searchString)) {
indexHtml = indexHtml.replace(searchString, `<div id="app">${homeContent}</div>`);
fs.writeFileSync(indexPath, indexHtml, 'utf-8');
console.log('✅ index.html обновлен с SSR контентом');
}
// 2. Страница terms
let termsHtml = indexHtml
.replace(homeContent, termsContent)
.replace('<title>bro-js admin</title>', '<title>Пользовательское соглашение - BROJS.RU</title>')
.replace(
'</head>',
'<meta name="description" content="Пользовательское соглашение для платформы обучения фронтенд-разработке BROJS.RU. Условия использования, обработка персональных данных, права и обязанности сторон." /></head>'
);
const termsPath = path.join(distPath, 'terms.html');
fs.writeFileSync(termsPath, termsHtml, 'utf-8');
console.log('✅ terms.html создан с SSR контентом');
console.log('🎉 SSR завершен успешно!');
console.log('📄 Созданы: index.html, terms.html');
console.log('💡 Весь контент отрендерен через React SSR');
} catch (error) {
console.error('❌ Ошибка при SSR:', error.message);
console.error(error.stack);
process.exit(1);
}