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
All checks were successful
platform/bro-js/bro.landing/pipeline/head This commit looks good
This commit is contained in:
@@ -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();
|
||||
@@ -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
93
scripts/ssr-render.js
Normal 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user