diff --git a/backend/package.json b/backend/package.json index 0f48246..8e69b8f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,7 +27,10 @@ "generate:flattened": "node scripts/generate-flattened.js", "compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened", "seed:legal": "node scripts/seed/legalTemplatesSeed.js", - "import:legal": "node scripts/import-legal-docs.js" + "import:legal": "node scripts/import-legal-docs.js", + "prerender:blog": "node scripts/pre-render-blog.js", + "prerender:blog:article": "node scripts/pre-render-blog.js --no-list", + "test:prerender": "node scripts/test-pre-render.js" }, "bin": {}, "engines": { @@ -66,6 +69,7 @@ "openai": "^4.102.0", "pg": "^8.10.0", "pg-large-object": "^2.0.0", + "puppeteer": "^24.15.0", "semver": "^7.7.1", "session-file-store": "^1.5.0", "siwe": "^2.1.4", diff --git a/backend/routes/pages.js b/backend/routes/pages.js index 7ea16cd..562c304 100644 --- a/backend/routes/pages.js +++ b/backend/routes/pages.js @@ -18,6 +18,7 @@ const path = require('path'); const multer = require('multer'); const vectorSearchClient = require('../services/vectorSearchClient'); const logger = require('../utils/logger'); +const { preRenderBlog } = require('../scripts/pre-render-blog'); const FIELDS_TO_EXCLUDE = ['image', 'tags']; @@ -42,7 +43,9 @@ async function ensureAdminPagesTable(fields) { 'id SERIAL PRIMARY KEY', 'author_address_encrypted TEXT NOT NULL', // Зашифрованный адрес автора 'created_at TIMESTAMP DEFAULT NOW()', - 'updated_at TIMESTAMP DEFAULT NOW()' + 'updated_at TIMESTAMP DEFAULT NOW()', + 'show_in_blog BOOLEAN DEFAULT FALSE', // Показывать в блоге + 'slug TEXT UNIQUE' // URL-friendly идентификатор для SEO ]; for (const field of fields) { columns.push(`"${field}_encrypted" TEXT`); @@ -63,6 +66,29 @@ async function ensureAdminPagesTable(fields) { ); } + // Добавляем поле show_in_blog если его нет (не зашифрованное поле) + if (!existingCols.includes('show_in_blog')) { + await db.getQuery()( + `ALTER TABLE ${tableName} ADD COLUMN show_in_blog BOOLEAN DEFAULT FALSE` + ); + } + + // Добавляем поле slug если его нет + if (!existingCols.includes('slug')) { + await db.getQuery()( + `ALTER TABLE ${tableName} ADD COLUMN slug TEXT` + ); + // Создаем уникальный индекс для slug + try { + await db.getQuery()( + `CREATE UNIQUE INDEX IF NOT EXISTS ${tableName}_slug_unique ON ${tableName}(slug) WHERE slug IS NOT NULL` + ); + } catch (e) { + // Индекс может уже существовать, игнорируем ошибку + console.log('[pages] Индекс для slug уже существует или ошибка создания:', e.message); + } + } + for (const field of fields) { const encryptedField = `${field}_encrypted`; if (!existingCols.includes(encryptedField)) { @@ -98,6 +124,87 @@ function stripHtml(html) { return String(html).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); } +/** + * Генерирует URL-friendly slug из текста + * @param {string} text - Исходный текст + * @param {number} maxLength - Максимальная длина slug (по умолчанию 100) + * @returns {string} - Сгенерированный slug + */ +function generateSlug(text, maxLength = 100) { + if (!text) return ''; + + return text + .toLowerCase() + .trim() + // Транслитерация кириллицы в латиницу + .replace(/[а-яё]/g, (char) => { + const map = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', + 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' + }; + return map[char] || char; + }) + // Заменяем все не-латинские символы и цифры на дефисы + .replace(/[^a-z0-9]+/g, '-') + // Убираем дефисы в начале и конце + .replace(/^-+|-+$/g, '') + // Ограничиваем длину + .substring(0, maxLength) + .replace(/-+$/, ''); // Убираем дефис в конце после обрезки +} + +/** + * Генерирует уникальный slug, проверяя существование в БД + * @param {string} title - Заголовок страницы + * @param {number} pageId - ID страницы (для исключения при проверке уникальности) + * @param {string} tableName - Имя таблицы + * @returns {Promise} - Уникальный slug + */ +async function generateUniqueSlug(title, pageId, tableName) { + let baseSlug = generateSlug(title); + if (!baseSlug) { + // Если slug пустой, используем id + baseSlug = `page-${pageId || Date.now()}`; + } + + let slug = baseSlug; + let counter = 1; + + // Проверяем уникальность + while (true) { + let query = `SELECT id FROM ${tableName} WHERE slug = $1`; + const params = [slug]; + + // Если это редактирование, исключаем текущую страницу + if (pageId) { + query += ` AND id != $2`; + params.push(pageId); + } + + const result = await db.getQuery()(query, params); + + if (result.rows.length === 0) { + // Slug уникален + return slug; + } + + // Slug уже существует, добавляем номер + slug = `${baseSlug}-${counter}`; + counter++; + + // Защита от бесконечного цикла + if (counter > 1000) { + slug = `${baseSlug}-${Date.now()}`; + break; + } + } + + return slug; +} + // Создать страницу (только для админа) router.post('/', upload.single('file'), async (req, res) => { console.log('[pages] POST /: Начало обработки запроса на создание страницы'); @@ -202,7 +309,8 @@ router.post('/', upload.single('file'), async (req, res) => { ? (() => { const parsed = parseInt(bodyRaw.order_index); return isNaN(parsed) ? 0 : parsed; })() : 0, nav_path: bodyRaw.nav_path || null, - is_index_page: bodyRaw.is_index_page === true || bodyRaw.is_index_page === 'true' + is_index_page: bodyRaw.is_index_page === true || bodyRaw.is_index_page === 'true', + show_in_blog: bodyRaw.show_in_blog === true || bodyRaw.show_in_blog === 'true' || bodyRaw.show_in_blog === true }; console.log('[pages] POST /: Создание страницы, данные:', { @@ -213,6 +321,14 @@ router.post('/', upload.single('file'), async (req, res) => { format: pageData.format }); + // Генерируем slug из заголовка (если не передан вручную) + let slug = bodyRaw.slug && bodyRaw.slug.trim() + ? bodyRaw.slug.trim() + : await generateUniqueSlug(pageData.title, null, tableName); + + // Добавляем slug в pageData + pageData.slug = slug; + // Формируем SQL для вставки данных (включаем все поля, даже null) // Фильтруем только undefined, null значения включаем (они допустимы в БД) const dataEntries = Object.entries(pageData).filter(([k, v]) => { @@ -261,6 +377,23 @@ router.post('/', upload.single('file'), async (req, res) => { // Индексация выполняется ТОЛЬКО вручную через кнопку "Индекс" (POST /:id/reindex) // Автоматическая индексация при создании отключена + // Запускаем pre-rendering для блога, если страница публичная и для блога + if (created.visibility === 'public' && + created.status === 'published' && + created.show_in_blog && + created.slug && + typeof created.slug === 'string' && + created.slug.trim() !== '') { + // Запускаем асинхронно, не блокируя ответ + preRenderBlog({ + renderList: true, + renderArticles: true, + specificSlug: created.slug.trim() + }).catch(err => { + console.error('[pages] Ошибка pre-rendering при создании страницы:', err); + }); + } + res.json(created); } catch (error) { console.error('[pages] Ошибка при создании страницы:', error); @@ -839,6 +972,26 @@ router.patch('/:id', upload.single('file'), async (req, res) => { if (FIELDS_TO_EXCLUDE.includes(k)) continue; if (k === 'required_permission') continue; // Уже обработано выше + // Обрабатываем show_in_blog как boolean + if (k === 'show_in_blog') { + updateData[k] = v === true || v === 'true' || v === 1 || v === '1'; + continue; + } + + // Обрабатываем slug + if (k === 'slug') { + if (v && String(v).trim()) { + // Если slug передан, проверяем уникальность + const uniqueSlug = await generateUniqueSlug(v, pageId, tableName); + updateData[k] = uniqueSlug; + } else if (incoming.title) { + // Если slug не передан, но есть title, генерируем из title + const uniqueSlug = await generateUniqueSlug(incoming.title, pageId, tableName); + updateData[k] = uniqueSlug; + } + continue; + } + // Нормализуем категорию: приводим к нижнему регистру для консистентности if (k === 'category') { updateData[k] = (v && String(v).trim()) ? String(v).trim().toLowerCase() : null; @@ -993,6 +1146,23 @@ router.patch('/:id', upload.single('file'), async (req, res) => { // Индексация выполняется ТОЛЬКО вручную через кнопку "Индекс" (POST /:id/reindex) // Автоматическая индексация при обновлении отключена + // Запускаем pre-rendering для блога, если страница публичная и для блога + if (updated.visibility === 'public' && + updated.status === 'published' && + updated.show_in_blog && + updated.slug && + typeof updated.slug === 'string' && + updated.slug.trim() !== '') { + // Запускаем асинхронно, не блокируя ответ + preRenderBlog({ + renderList: true, + renderArticles: true, + specificSlug: updated.slug.trim() + }).catch(err => { + console.error('[pages] Ошибка pre-rendering при обновлении страницы:', err); + }); + } + res.json(updated); } catch (error) { console.error('[pages] PATCH /:id: Ошибка при обновлении страницы:', error); @@ -1210,6 +1380,216 @@ router.get('/public/all', async (req, res) => { } }); +// Получить все страницы блога (только с show_in_blog = true) +router.get('/blog/all', async (req, res) => { + try { + const tableName = `admin_pages_simple`; + + // Проверяем, есть ли таблица + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + + if (!existsRes.rows[0].exists) { + return res.json([]); + } + + // Поддержка фильтрации по категории и поиску + const { category, search } = req.query; + let whereClause = `WHERE visibility = 'public' AND status = 'published' AND show_in_blog = TRUE`; + const params = []; + let paramIndex = 1; + + if (category) { + whereClause += ` AND category = $${paramIndex}`; + params.push(category); + paramIndex++; + } + + if (search) { + whereClause += ` AND (title ILIKE $${paramIndex} OR summary ILIKE $${paramIndex})`; + params.push(`%${search}%`); + paramIndex++; + } + + // Сортировка: сначала по дате создания (новые первыми), затем по order_index + const { rows } = await db.getQuery()(` + SELECT * FROM ${tableName} + ${whereClause} + ORDER BY + created_at DESC, + COALESCE(order_index, 0) ASC + `, params); + + console.log(`[pages] GET /blog/all: найдено ${rows.length} страниц блога`); + + // Обрабатываем результаты: генерируем slug для страниц, у которых его нет + const processedRows = await Promise.all(rows.map(async (row) => { + // Если у страницы нет slug, генерируем его из title + if (!row.slug || row.slug.trim() === '') { + try { + // Получаем расшифрованный title для генерации slug + const encryptionUtils = require('../utils/encryptionUtils'); + const encryptionKey = encryptionUtils.getEncryptionKey(); + + // Расшифровываем title (если он зашифрован) + let title = row.title; + if (row.title_encrypted) { + const titleResult = await db.getQuery()( + `SELECT decrypt_text($1, $2) as title`, + [row.title_encrypted, encryptionKey] + ); + title = titleResult.rows[0]?.title || row.title || `page-${row.id}`; + } + + // Генерируем slug + const newSlug = await generateUniqueSlug(title, row.id, tableName); + + // Обновляем slug в БД + await db.getQuery()( + `UPDATE ${tableName} SET slug = $1 WHERE id = $2`, + [newSlug, row.id] + ); + + // Обновляем slug в объекте row + row.slug = newSlug; + + console.log(`[pages] GET /blog/all: сгенерирован slug "${newSlug}" для страницы ${row.id}`); + } catch (error) { + console.error(`[pages] GET /blog/all: ошибка генерации slug для страницы ${row.id}:`, error); + // Если не удалось сгенерировать slug, используем id как fallback + row.slug = `page-${row.id}`; + } + } + + return row; + })); + + res.json(processedRows); + } catch (error) { + console.error('Ошибка получения страниц блога:', error); + res.status(500).json([]); + } +}); + +// Получить страницу блога по slug +router.get('/blog/:slug', async (req, res) => { + try { + const tableName = `admin_pages_simple`; + let slug = req.params.slug; + + // Декодируем slug (на случай если он был закодирован) + try { + slug = decodeURIComponent(slug); + } catch (e) { + // Если декодирование не удалось, используем как есть + console.warn('[pages] Ошибка декодирования slug:', e.message); + } + + // Валидация slug + if (!slug || typeof slug !== 'string' || slug.trim() === '') { + return res.status(400).json({ error: 'Невалидный slug' }); + } + + slug = slug.trim(); + + // Проверяем, есть ли таблица + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + + if (!existsRes.rows[0].exists) { + return res.status(404).json({ error: 'Страница не найдена' }); + } + + // Получаем страницу по slug + const { rows } = await db.getQuery()( + `SELECT * FROM ${tableName} + WHERE slug = $1 + AND visibility = 'public' + AND status = 'published' + AND show_in_blog = TRUE + LIMIT 1`, + [slug] + ); + + console.log(`[pages] GET /blog/:slug: поиск по slug "${slug}", найдено строк: ${rows.length}`); + + if (rows.length === 0) { + // Пробуем найти страницу без учета регистра и пробелов + const { rows: rowsCaseInsensitive } = await db.getQuery()( + `SELECT * FROM ${tableName} + WHERE LOWER(TRIM(slug)) = LOWER(TRIM($1)) + AND visibility = 'public' + AND status = 'published' + AND show_in_blog = TRUE + LIMIT 1`, + [slug] + ); + + if (rowsCaseInsensitive.length > 0) { + console.log(`[pages] GET /blog/:slug: найдено с учетом регистра, slug в БД: "${rowsCaseInsensitive[0].slug}"`); + return res.json(rowsCaseInsensitive[0]); + } + + // Показываем все доступные slug для отладки (только в dev режиме) + if (process.env.NODE_ENV !== 'production') { + const { rows: allSlugs } = await db.getQuery()( + `SELECT id, slug, title FROM ${tableName} + WHERE visibility = 'public' + AND status = 'published' + AND show_in_blog = TRUE + LIMIT 10` + ); + console.log(`[pages] GET /blog/:slug: доступные slug:`, allSlugs.map(r => ({ id: r.id, slug: r.slug }))); + } + + return res.status(404).json({ error: 'Страница не найдена' }); + } + + console.log(`[pages] GET /blog/:slug: страница найдена, id: ${rows[0].id}, slug: ${rows[0].slug}`); + + // Расшифровываем зашифрованные поля + const page = rows[0]; + const encryptionUtils = require('../utils/encryptionUtils'); + const encryptionKey = encryptionUtils.getEncryptionKey(); + + // Создаем объект с расшифрованными данными + const decryptedPage = { ...page }; + + // Расшифровываем поля, если они зашифрованы + const fieldsToDecrypt = ['title', 'summary', 'content', 'seo', 'settings']; + for (const field of fieldsToDecrypt) { + const encryptedField = `${field}_encrypted`; + if (page[encryptedField]) { + try { + const decryptResult = await db.getQuery()( + `SELECT decrypt_text($1, $2) as ${field}`, + [page[encryptedField], encryptionKey] + ); + if (decryptResult.rows[0] && decryptResult.rows[0][field] !== null) { + decryptedPage[field] = decryptResult.rows[0][field]; + } + } catch (decryptError) { + console.warn(`[pages] GET /blog/:slug: ошибка расшифровки поля ${field}:`, decryptError.message); + // Если расшифровка не удалась, оставляем оригинальное значение или null + if (page[field]) { + decryptedPage[field] = page[field]; + } + } + } else if (page[field]) { + // Если поле не зашифровано, используем его как есть + decryptedPage[field] = page[field]; + } + } + + res.json(decryptedPage); + } catch (error) { + console.error('Ошибка получения страницы блога по slug:', error); + res.status(500).json({ error: 'Ошибка получения страницы' }); + } +}); + // Получить иерархическую структуру всех публичных страниц router.get('/public/structure', async (req, res) => { try { @@ -1562,6 +1942,7 @@ router.get('/public/robots.txt', async (req, res) => { const robotsContent = `User-agent: * Allow: / +Allow: /blog Allow: /content/published Disallow: /api/ Disallow: /ws @@ -1569,7 +1950,7 @@ Disallow: /admin/ Disallow: /content/create Disallow: /content/edit -Sitemap: ${baseUrl}/sitemap.xml +Sitemap: ${baseUrl}/pages/public/sitemap.xml `; res.setHeader('Content-Type', 'text/plain'); @@ -1593,18 +1974,6 @@ router.get('/public/sitemap.xml', async (req, res) => { `SELECT to_regclass($1) as exists`, [tableName] ); - let pages = []; - if (existsRes.rows[0].exists) { - // Получаем все опубликованные публичные страницы - const { rows } = await db.getQuery()(` - SELECT id, title, updated_at, created_at - FROM ${tableName} - WHERE status = 'published' AND visibility = 'public' - ORDER BY created_at DESC - `); - pages = rows; - } - // Генерируем XML sitemap let sitemap = ` @@ -1613,6 +1982,11 @@ router.get('/public/sitemap.xml', async (req, res) => { daily 1.0 + + ${baseUrl}/blog + daily + 0.9 + ${baseUrl}/content/published daily @@ -1620,8 +1994,41 @@ router.get('/public/sitemap.xml', async (req, res) => { `; - // Добавляем страницы документов - for (const page of pages) { + if (existsRes.rows[0].exists) { + // Получаем страницы блога (с show_in_blog = true) + const { rows: blogPages } = await db.getQuery()(` + SELECT id, slug, updated_at, created_at + FROM ${tableName} + WHERE status = 'published' AND visibility = 'public' AND show_in_blog = TRUE + ORDER BY created_at DESC + `); + + // Добавляем страницы блога с использованием slug + for (const page of blogPages) { + const lastmod = page.updated_at || page.created_at || new Date().toISOString(); + const pageUrl = page.slug + ? `${baseUrl}/blog/${page.slug}` + : `${baseUrl}/blog?page=${page.id}`; + + sitemap += ` + ${escapeXml(pageUrl)} + ${lastmod.split('T')[0]} + weekly + 0.8 + +`; + } + + // Получаем остальные публичные страницы (без show_in_blog) + const { rows: otherPages } = await db.getQuery()(` + SELECT id, updated_at, created_at + FROM ${tableName} + WHERE status = 'published' AND visibility = 'public' AND (show_in_blog IS NULL OR show_in_blog = FALSE) + ORDER BY created_at DESC + `); + + // Добавляем остальные публичные страницы + for (const page of otherPages) { const lastmod = page.updated_at || page.created_at || new Date().toISOString(); const pageUrl = `${baseUrl}/content/published?page=${page.id}`; @@ -1632,6 +2039,7 @@ router.get('/public/sitemap.xml', async (req, res) => { 0.6 `; + } } sitemap += ``; @@ -1658,4 +2066,62 @@ function escapeXml(unsafe) { }); } +// Endpoint для ручного запуска pre-rendering блога +router.post('/blog/prerender', async (req, res) => { + try { + // Проверка прав доступа (только админ) + if (!req.session || !req.session.authenticated || !req.session.address) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + + const authService = require('../services/auth-service'); + const userAccessLevel = await authService.getUserAccessLevel(req.session.address); + + if (!userAccessLevel.hasAccess) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + // Парсим параметры + let { renderList = true, renderArticles = true, specificSlug = null } = req.body; + + // Валидация параметров + renderList = Boolean(renderList); + renderArticles = Boolean(renderArticles); + + // Валидация slug + if (specificSlug && (typeof specificSlug !== 'string' || specificSlug.trim() === '')) { + return res.status(400).json({ error: 'Невалидный slug' }); + } + if (specificSlug) { + specificSlug = specificSlug.trim(); + } + + console.log('[pages] POST /blog/prerender: Запуск pre-rendering...', { + renderList, + renderArticles, + specificSlug: specificSlug || 'all' + }); + + // Запускаем pre-rendering асинхронно + preRenderBlog({ + renderList, + renderArticles, + specificSlug + }).then(() => { + console.log('[pages] POST /blog/prerender: Pre-rendering завершен успешно'); + }).catch(err => { + console.error('[pages] POST /blog/prerender: Ошибка pre-rendering:', err); + }); + + // Возвращаем ответ сразу, не дожидаясь завершения + res.json({ + success: true, + message: 'Pre-rendering запущен. Проверьте логи для деталей.' + }); + } catch (error) { + console.error('[pages] POST /blog/prerender: Ошибка:', error); + res.status(500).json({ error: 'Ошибка запуска pre-rendering' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/scripts/pre-render-blog.js b/backend/scripts/pre-render-blog.js new file mode 100644 index 0000000..2a43fda --- /dev/null +++ b/backend/scripts/pre-render-blog.js @@ -0,0 +1,321 @@ +/** + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + * All rights reserved. + * + * This software is proprietary and confidential. + * Unauthorized copying, modification, or distribution is prohibited. + * + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/VC-HB3-Accelerator + */ + +const puppeteer = require('puppeteer'); +const fs = require('fs'); +const path = require('path'); +const { initDbPool, getQuery } = require('../db'); + +// Конфигурация +// В Docker используем имя контейнера, локально - localhost +const BASE_URL = process.env.PRERENDER_BASE_URL || + process.env.FRONTEND_URL || + (process.env.NODE_ENV === 'production' ? 'http://dapp-frontend:5173' : 'http://localhost:5173'); +const OUTPUT_DIR = process.env.PRERENDER_OUTPUT_DIR || + (process.env.NODE_ENV === 'production' + ? '/app/frontend_dist/blog' + : path.join(__dirname, '../../frontend/dist/blog')); +const TIMEOUT = 30000; // 30 секунд на загрузку страницы + +/** + * Создает директорию если её нет + */ +function ensureDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +/** + * Ожидает загрузки контента на странице + */ +async function waitForContent(page, selector, timeout = TIMEOUT) { + try { + await page.waitForSelector(selector, { timeout }); + // Дополнительная задержка для полной загрузки контента + // Используем setTimeout вместо устаревшего waitForTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch (error) { + console.warn(`[pre-render] Селектор ${selector} не найден, продолжаем...`); + } +} + +/** + * Рендерит страницу и возвращает HTML + */ +async function renderPage(browser, url, options = {}) { + if (!browser) { + throw new Error('Браузер не инициализирован'); + } + + const page = await browser.newPage(); + + try { + // Устанавливаем viewport + await page.setViewport({ width: 1920, height: 1080 }); + + // Устанавливаем таймауты + page.setDefaultNavigationTimeout(TIMEOUT); + page.setDefaultTimeout(TIMEOUT); + + // Переходим на страницу + console.log(`[pre-render] Загрузка: ${url}`); + await page.goto(url, { + waitUntil: 'networkidle2', + timeout: TIMEOUT + }); + + // Ждем загрузки контента + if (options.waitForSelector) { + await waitForContent(page, options.waitForSelector); + } + + // Получаем HTML + const html = await page.content(); + + if (!html || html.trim().length === 0) { + throw new Error('Получен пустой HTML'); + } + + // Оптимизируем HTML: удаляем скрипты, которые не нужны для SEO + const optimizedHtml = optimizeHtml(html, url); + + return optimizedHtml; + } catch (error) { + console.error(`[pre-render] Ошибка при рендеринге ${url}:`, error.message); + throw error; + } finally { + try { + await page.close(); + } catch (closeError) { + console.warn(`[pre-render] Ошибка при закрытии страницы: ${closeError.message}`); + } + } +} + +/** + * Оптимизирует HTML для статического контента + */ +function optimizeHtml(html, url) { + // Удаляем скрипты, которые выполняются только на клиенте + // Но оставляем мета-теги и структуру + let optimized = html; + + // Удаляем inline скрипты, которые не нужны для SEO + optimized = optimized.replace(/]*>(?!.*application\/ld\+json)[\s\S]*?<\/script>/gi, ''); + + // Оставляем JSON-LD разметку + optimized = optimized.replace(/]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi, + ''); + + // Удаляем WebSocket подключения + optimized = optimized.replace(/new WebSocket\([^)]+\)/gi, ''); + + // Удаляем console.log для уменьшения размера + optimized = optimized.replace(/console\.(log|warn|error)\([^)]*\)/gi, ''); + + return optimized; +} + +/** + * Очищает slug для использования в имени файла + */ +function sanitizeSlug(slug) { + if (!slug) return 'page'; + // Удаляем небезопасные символы для имени файла + return slug.replace(/[^a-z0-9\-_]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); +} + +/** + * Сохраняет HTML в файл + */ +function saveHtml(html, filePath) { + try { + ensureDir(path.dirname(filePath)); + + // Проверяем, что путь безопасен (защита от path traversal) + const resolvedPath = path.resolve(filePath); + const outputDirResolved = path.resolve(OUTPUT_DIR); + + if (!resolvedPath.startsWith(outputDirResolved)) { + throw new Error(`Небезопасный путь: ${filePath}`); + } + + fs.writeFileSync(resolvedPath, html, 'utf8'); + console.log(`[pre-render] Сохранено: ${resolvedPath}`); + } catch (error) { + console.error(`[pre-render] Ошибка сохранения файла ${filePath}:`, error.message); + throw error; + } +} + +/** + * Получает список статей блога из БД + */ +async function getBlogArticles() { + try { + const query = getQuery(); + if (!query) { + console.error('[pre-render] БД не инициализирована'); + return []; + } + + const tableName = 'admin_pages_simple'; + + const { rows } = await query(` + SELECT id, slug, title + FROM ${tableName} + WHERE visibility = 'public' + AND status = 'published' + AND show_in_blog = TRUE + AND slug IS NOT NULL + AND slug != '' + ORDER BY created_at DESC + `); + + return rows || []; + } catch (error) { + console.error('[pre-render] Ошибка получения статей из БД:', error); + return []; + } +} + +/** + * Основная функция pre-rendering + */ +async function preRenderBlog(options = {}) { + const { + renderList = true, + renderArticles = true, + specificSlug = null + } = options; + + console.log('[pre-render] Начало pre-rendering блога...'); + console.log(`[pre-render] Base URL: ${BASE_URL}`); + console.log(`[pre-render] Output dir: ${OUTPUT_DIR}`); + + // Создаем директорию для вывода + ensureDir(OUTPUT_DIR); + + // Инициализируем браузер + let browser; + try { + browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-software-rasterizer', + '--disable-extensions' + ] + }); + } catch (error) { + console.error('[pre-render] Ошибка запуска браузера:', error.message); + throw new Error(`Не удалось запустить браузер: ${error.message}`); + } + + try { + // Рендерим список статей + if (renderList) { + console.log('[pre-render] Рендеринг списка статей...'); + const listHtml = await renderPage(browser, `${BASE_URL}/blog`, { + waitForSelector: '.blog-articles, .empty-state, .loading-state' + }); + saveHtml(listHtml, path.join(OUTPUT_DIR, 'index.html')); + } + + // Получаем список статей + const articles = await getBlogArticles(); + console.log(`[pre-render] Найдено статей: ${articles.length}`); + + if (renderArticles && articles.length > 0) { + // Рендерим статьи + let articlesToRender; + if (specificSlug) { + // Фильтруем по slug (точное совпадение) + articlesToRender = articles.filter(a => a.slug && a.slug.trim() === specificSlug.trim()); + if (articlesToRender.length === 0) { + console.warn(`[pre-render] Статья со slug "${specificSlug}" не найдена`); + } + } else { + articlesToRender = articles; + } + + for (const article of articlesToRender) { + try { + // Проверяем, что slug валидный + if (!article.slug || typeof article.slug !== 'string' || article.slug.trim() === '') { + console.warn(`[pre-render] Пропущена статья с невалидным slug: ${article.id}`); + continue; + } + + const sanitizedSlug = sanitizeSlug(article.slug); + console.log(`[pre-render] Рендеринг статьи: ${sanitizedSlug} (${article.title})`); + + const articleHtml = await renderPage(browser, `${BASE_URL}/blog/${encodeURIComponent(article.slug)}`, { + waitForSelector: '.docs-content, .article-view' + }); + + // Сохраняем в файл с именем slug (используем sanitized slug для безопасности) + const filePath = path.join(OUTPUT_DIR, `${sanitizedSlug}.html`); + saveHtml(articleHtml, filePath); + } catch (error) { + console.error(`[pre-render] Ошибка при рендеринге статьи ${article.slug}:`, error.message); + // Продолжаем с другими статьями + } + } + } + + console.log('[pre-render] Pre-rendering завершен успешно!'); + } catch (error) { + console.error('[pre-render] Критическая ошибка:', error); + throw error; + } finally { + // Закрываем браузер, если он был открыт + if (browser) { + try { + await browser.close(); + } catch (closeError) { + console.error('[pre-render] Ошибка при закрытии браузера:', closeError.message); + } + } + } +} + +// Если скрипт запущен напрямую +if (require.main === module) { + (async () => { + try { + // Инициализируем БД + await initDbPool(); + + // Парсим аргументы командной строки + const args = process.argv.slice(2); + const options = { + renderList: !args.includes('--no-list'), + renderArticles: !args.includes('--no-articles'), + specificSlug: args.find(arg => arg.startsWith('--slug='))?.split('=')[1] || null + }; + + await preRenderBlog(options); + process.exit(0); + } catch (error) { + console.error('[pre-render] Фатальная ошибка:', error); + process.exit(1); + } + })(); +} + +module.exports = { preRenderBlog }; + diff --git a/backend/yarn.lock b/backend/yarn.lock index bf39702..bcaa32f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -17,6 +17,20 @@ resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.51.0.tgz#2cd022c47e0eb6f4d645d8e8ed9ee0f3c5745ea8" integrity sha512-fAFC/uHhyzfw7rs65EPVV+scXDytGNm5BjttxHf6rP/YGvaBRKEvp2lwyuMigTwMI95neeG4bzrZigz7KCikjw== +"@babel/code-frame@^7.0.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/helper-validator-identifier@^7.27.1": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -876,6 +890,19 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@puppeteer/browsers@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.11.0.tgz#b2dcd7cb02dd2de5909531d00e717a04bd61de73" + integrity sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ== + dependencies: + debug "^4.4.3" + extract-zip "^2.0.1" + progress "^2.0.3" + proxy-agent "^6.5.0" + semver "^7.7.3" + tar-fs "^3.1.1" + yargs "^17.7.2" + "@scure/base@~1.1.0", "@scure/base@~1.1.6": version "1.1.9" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" @@ -1058,6 +1085,11 @@ resolved "https://registry.yarnpkg.com/@telegraf/types/-/types-7.1.0.tgz#d8bd9b2f5070b4de46971416e890338cd89fc23d" integrity sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -1222,6 +1254,13 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== +"@types/yauzl@^2.9.1": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== + dependencies: + "@types/node" "*" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1296,7 +1335,7 @@ agent-base@6: dependencies: debug "4" -agent-base@^7.1.2: +agent-base@^7.1.0, agent-base@^7.1.2: version "7.1.4" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== @@ -1535,6 +1574,13 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -1660,6 +1706,11 @@ base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -1831,6 +1882,11 @@ buffer-crc32@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-equal-constant-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -1997,6 +2053,14 @@ chokidar@^4.0.0: dependencies: readdirp "^4.0.1" +chromium-bidi@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-12.0.1.tgz#3edb1420ab5a52004c10c223b928622c128b4f27" + integrity sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg== + dependencies: + mitt "^3.0.1" + zod "^3.24.1" + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -2218,6 +2282,16 @@ cors@^2.8.5: object-assign "^4" vary "^1" +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + crc-32@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" @@ -2319,6 +2393,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" @@ -2358,7 +2437,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: +debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2422,6 +2501,15 @@ define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2442,6 +2530,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +devtools-protocol@0.0.1534754: + version "0.0.1534754" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz#75fb0496ff133d8d7e73d2e49600b37fcb4f46a9" + integrity sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -2598,11 +2691,18 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -env-paths@^2.2.0: +env-paths@^2.2.0, env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: version "1.24.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" @@ -2738,6 +2838,17 @@ escodegen@1.8.x: optionalDependencies: source-map "~0.2.0" +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^10.0.2: version "10.1.8" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" @@ -2816,7 +2927,7 @@ esprima@2.7.x, esprima@^2.7.1: resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" integrity sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A== -esprima@^4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -3041,6 +3152,17 @@ extend@^3.0.2, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -3094,6 +3216,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" @@ -3366,6 +3495,13 @@ get-proto@^1.0.0, get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -3375,6 +3511,15 @@ get-symbol-description@^1.1.0: es-errors "^1.3.0" get-intrinsic "^1.2.6" +get-uri@^6.0.1: + version "6.0.5" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16" + integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -3783,6 +3928,14 @@ http-errors@~1.7.3: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-signature@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" @@ -3800,7 +3953,7 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^7.0.1: +https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== @@ -3862,7 +4015,7 @@ immutable@^4.0.0-rc.12: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== @@ -3924,6 +4077,11 @@ io-ts@1.10.4: dependencies: fp-ts "^1.0.0" +ip-address@^10.0.1: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4" + integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -3938,6 +4096,11 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: call-bound "^1.0.3" get-intrinsic "^1.2.6" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-arrayish@^0.3.1: version "0.3.4" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" @@ -4204,6 +4367,11 @@ js-tiktoken@^1.0.12: dependencies: base64-js "^1.5.1" +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@3.x: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -4236,6 +4404,11 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4452,6 +4625,11 @@ libqp@2.1.1: resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.1.1.tgz#f1be767a58f966f500597997cab72cfc1e17abfa" integrity sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow== +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + linkify-it@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" @@ -4533,6 +4711,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -4713,6 +4896,11 @@ minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + mkdirp@0.5.x, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -4822,6 +5010,11 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + nice-grpc-client-middleware-retry@^3.1.11: version "3.1.11" resolved "https://registry.yarnpkg.com/nice-grpc-client-middleware-retry/-/nice-grpc-client-middleware-retry-3.1.11.tgz#4fc0128b891d184b6c98af3bfd6aca1b608a3fd1" @@ -5157,6 +5350,28 @@ p-timeout@^4.1.0: resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== +pac-proxy-agent@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df" + integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.1.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.6" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.5" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" @@ -5169,6 +5384,16 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parseley@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" @@ -5242,6 +5467,11 @@ peberminta@^0.9.0: resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352" integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ== +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -5308,7 +5538,7 @@ pgpass@1.0.5: dependencies: split2 "^4.1.0" -picocolors@^1.1.0: +picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -5385,6 +5615,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -5419,6 +5654,20 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-agent@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d" + integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.6" + lru-cache "^7.14.1" + pac-proxy-agent "^7.1.0" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.5" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -5462,6 +5711,31 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +puppeteer-core@24.34.0: + version "24.34.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-24.34.0.tgz#00c7f63b4a83d4ca2ec5ea3a234588fb2ce7c994" + integrity sha512-24evawO+mUGW4mvS2a2ivwLdX3gk8zRLZr9HP+7+VT2vBQnm0oh9jJEZmUE3ePJhRkYlZ93i7OMpdcoi2qNCLg== + dependencies: + "@puppeteer/browsers" "2.11.0" + chromium-bidi "12.0.1" + debug "^4.4.3" + devtools-protocol "0.0.1534754" + typed-query-selector "^2.12.0" + webdriver-bidi-protocol "0.3.10" + ws "^8.18.3" + +puppeteer@^24.15.0: + version "24.34.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-24.34.0.tgz#061f6e97ce9511863ec83cd6f17a27253c68b5e9" + integrity sha512-Sdpl/zsYOsagZ4ICoZJPGZw8d9gZmK5DcxVal11dXi/1/t2eIXHjCf5NfmhDg5XnG9Nye+yo/LqMzIxie2rHTw== + dependencies: + "@puppeteer/browsers" "2.11.0" + chromium-bidi "12.0.1" + cosmiconfig "^9.0.0" + devtools-protocol "0.0.1534754" + puppeteer-core "24.34.0" + typed-query-selector "^2.12.0" + qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -5816,7 +6090,7 @@ selderee@^0.11.0: dependencies: parseley "^0.12.0" -semver@^5.5.0, semver@^6.3.0, semver@^7.3.4, semver@^7.5.3, semver@^7.6.3, semver@^7.7.1, semver@~5.3.0: +semver@^5.5.0, semver@^6.3.0, semver@^7.3.4, semver@^7.5.3, semver@^7.6.3, semver@^7.7.1, semver@^7.7.3, semver@~5.3.0: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== @@ -6051,6 +6325,28 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.7" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + dependencies: + ip-address "^10.0.1" + smart-buffer "^4.2.0" + solc@0.8.26: version "0.8.26" resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" @@ -6097,7 +6393,7 @@ source-map-support@^0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -6360,7 +6656,7 @@ table@^6.8.0: string-width "^4.2.3" strip-ansi "^6.0.1" -tar-fs@^3.0.0: +tar-fs@^3.0.0, tar-fs@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.1.tgz#4f164e59fb60f103d472360731e8c6bb4a7fe9ef" integrity sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg== @@ -6552,6 +6848,11 @@ tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -6677,6 +6978,11 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +typed-query-selector@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6901,6 +7207,11 @@ web3-utils@^1.3.6: randombytes "^2.1.0" utf8 "3.0.0" +webdriver-bidi-protocol@0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.10.tgz#437405564ff7e200371468f4f1eba1ff5537e119" + integrity sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -7084,7 +7395,7 @@ write-file-atomic@3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@8.17.1, ws@8.18.3, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1: +ws@8.17.1, ws@8.18.3, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1, ws@^8.18.3: version "8.18.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== @@ -7150,6 +7461,14 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -7174,7 +7493,7 @@ zod-to-json-schema@^3.22.3: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d" integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg== -zod@^3.22.4, zod@^3.25.32: +zod@^3.22.4, zod@^3.24.1, zod@^3.25.32: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== diff --git a/frontend/nginx-simple.conf b/frontend/nginx-simple.conf index ccdee83..5a57ad0 100644 --- a/frontend/nginx-simple.conf +++ b/frontend/nginx-simple.conf @@ -141,6 +141,17 @@ http { add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; } + # Pre-rendered blog pages (SEO optimization) + location /blog { + root /usr/share/nginx/html; + try_files $uri $uri.html /blog/index.html /index.html; + + # Заголовки для SEO + add_header Cache-Control "public, max-age=3600"; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + } + # Статические файлы location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 39b65c5..ed177ff 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -47,7 +47,7 @@ Чат - + Блог diff --git a/frontend/src/components/docs/DocsContent.vue b/frontend/src/components/docs/DocsContent.vue index 7ba7e12..79e6c5f 100644 --- a/frontend/src/components/docs/DocsContent.vue +++ b/frontend/src/components/docs/DocsContent.vue @@ -30,7 +30,7 @@