У меня есть проблема, что во всех частях сайта тайтл один и тот же "AIChat —...

📅 09.12.2025, 02:52:26
у меня есть проблема, что во всех частях сайта тайтл один и тот же "AIChat — персональный ChatGPT на русском без VPN", главную мы оптимизировали под поиск Яндекс и Гугл, а вот публичные чаты нет. Файлы закрепил [Пользователь прикрепил следующие файлы: - index.ejs (9 КБ) - public-chats.ejs (7 КБ) - public-chat-view.ejs (7 КБ) - chat.ejs (20 КБ) - header.ejs (7 КБ) Содержимое файла "index.ejs" (фрагмент): <%- include("partials/header") %> <main class="site-main"> <!-- HERO --> <section class="hero"> <div class="container hero-inner"> <div class="hero-text"> <h1>Твой персональный доступ к ChatGPT</h1> <p> Быстрый и удобный ChatGPT, заточенный под твои задачи. </p> <div class="hero-actions"> <a href="/register" class="btn-primary">Регистрация</a> <a href="/login" class="btn-secondary">Войти</a> </div> <ul class="hero-benefits"> <li>⚡ На базе новейших моделей</li> <li>📊 Доступные тарифы</li> <li>🔒 Работает без VPN</li> </ul> </div> <div class="hero-card"> <div class="hero-preview-body"> <div class="bubble bubble-user"> Напиши, как ты можешь помочь? </div> <div class="bubble bubble-ai"> Я могу отвечать на вопросы, помогать с текстами и кодом, анализировать данные и многое другое 🚀 </div> </div> </div> </div> </section> <!-- FREE TRY --> <section class="section"> <div class="container"> <div class="card free-card"> <h2>Попробуй прямо сейчас — без регистрации</h2> <p class="card-subtitle"> Для полного функционала создай аккаунт. </p> <% let defaultFreeKey = ""; for (let i = 0; i < models.length; i++) { if (models[i].credits === 1) { defaultFreeKey = models[i].model_key; break; } } if (!defaultFreeKey && models[0]) defaultFreeKey = models[0].model_key; %> <!-- ОБЛАСТЬ ДЕМО-ЧАТА --> <div id="free-chat-answer" class="free-answer empty"></div> <div class="free-input-panel"> <div class="model-picker"> <div class="model-picker-top-row"> <span class="model-picker-title">Модели</span> <span class="model-picker-credits"> Бесплатный лимит: <strong><%= credits %></strong> кредит<%= credits === 1 ? '' : 'ов' %>. </span> </div> <div class="model-pill-row"> <% models.forEach(function(m) { const canUse = (m.credits === 1 && credits >= 1); const disabled = !canUse; const isActive = (!disabled && m.model_key === defaultFreeKey); %> <button type="button" class="model-pill model-tile-free <%= isActive ? 'is-active' : '' %> <%= disabled ? 'is-disabled' : '' %>" data-model-key="<%= m.model_key %>" data-credits="<%= m.credits %>" <%= disabled ? 'disabled' : '' %> > <span><%= m.label %> (<%= m.credits %>)</span> </button> <% }) %> </div> <div id="image-viewer" class="image-viewer hidden"> <div class="image-viewer-dialog"> <div class="image-viewer-topbar"> <div class="image-viewer-title">Просмотр изображения</div> <div class="image-viewer-actions"> <button type="button" class="iv-btn" id="iv-zoom-out">−</button> <button type="button" class="iv-btn" id="iv-zoom-in">+</button> <button type="button" class="iv-btn iv-close" id="iv-close">✕</button> </div> </div> <div class="image-viewer-body"> <div class="image-viewer-scroll"> <div class="image-viewer-stage" id="iv-stage"> <img id="image-viewer-img" alt="Изображение" /> <canvas id="image-viewer-canvas"></canvas> </div> </div> </div> <div class="image-viewer-toolbar"> <button type="button" class="iv-btn" id="iv-edit-toggle"> Изменить </button> <div class="iv-marker-controls" id="iv-marker-controls"> Толщина маркера: <input type="range" id="iv-marker-size" min="2" max="40" value="10" /> <span id="iv-marker-size-label">10 px</span> <button type="button" class="iv-btn" id="iv-clear" > Очистить </button> </div> </div> <div class="image-viewer-edit-panel" id="iv-edit-panel"> <label class="iv-edit-label"> Опиши, что нужно изменить на картинке: <textarea id="iv-prompt" placeholder="Например: «Поменяй текст на вывеске на &quot;Открыто&quot; и убери лишние объекты на заднем плане»" ></textarea> </label> <button type="button" class="btn-primary iv-apply-btn" id="iv-apply" > Исправить </button> </div> </div> </div> <input type="hidden" id="free-chat-model" value="<%= defaultFreeKey %>" /> <div class="model-hint"> В демо доступны только модели за <strong>1</strong> кредит. Повторно бесплатный кредит получить нельзя. </div> </div> <form id="free-chat-form" data-auth="<%= isAuthenticated ? '1' : '0' %>"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <!-- выбранные файлы НАД вводом --> <div class="attach-list" id="free-attach-list"></div> <div class="input-row"> <div class="input-row-field"> <textarea name="message" required placeholder="Задай любой вопрос, например: «Составь короткое продающее описание моего продукта»" ></textarea> <button type="button" class="icon-btn input-attach" id="free-attach-btn" title="Прикрепить файл" >📎</button> </div> </div> <input type="file" id="free-file-input" class="hidden-file-input" multiple /> <button type="submit" class="btn-primary btn-full" data-default-label="Спросить ИИ" >Спросить ИИ</button> </form> </div> </div> </div> </section> <!-- FEATURES --> <section class="section section-dark"> <div class="container"> <div class="features"> <div class="feature-card"> <h3>Всегда свежие модели GPT</h3> <p>Все новые модели ChatGPT в товоем личном кабинете.</p> </div> <div class="feature-card"> <h3>Выгодные тарифы</h3> <p>Доступные тарифы, выбери удобный для своих задач.</p> </div> <div class="feature-card"> <h3>Работает без VPN</h3> <p>Сервис досупен в России без VPN с оплатой российскими картами.</p> </div> </div> </div> </section> </main> <%- include("partials/footer") %> Содержимое файла "public-chats.ejs" (фрагмент): <%- include("partials/header") %> <main class="site-main"> <section class="section"> <div class="container"> <div class="card public-page-card"> <!-- ХЕРО / ШАПКА --> <header class="public-header"> <div class="public-header-main"> <h1>Публичные вопросы к ИИ</h1> <p class="card-subtitle"> Вопросы пользователей с открытым доступом </p> </div> <div class="public-header-meta"> <a href="/chat" class="btn-primary"> Задать вопрос </a> </div> </header> <% if (!chats || !chats.length) { %> <div class="public-empty"> Пока нет публичных вопросов. </div> <% } else { %> <!-- ПАНЕЛЬ НАВИГАЦИИ: поиск + фильтры + сортировка --> <div class="public-toolbar"> <div class="public-search"> <input type="search" class="public-search-input" placeholder="Поиск по заголовкам вопросов..." /> </div> <div class="public-toolbar-right"> <div class="public-filters"> <button type="button" class="public-filter-btn is-active" data-range="all" > Все </button> <button type="button" class="public-filter-btn" data-range="day" > За день </button> <button type="button" class="public-filter-btn" data-range="week" > За неделю </button> <button type="button" class="public-filter-btn" data-range="month" > За месяц </button> </div> <div class="public-sort"> <label> <span>Сортировка:</span> <select class="public-sort-select"> <option value="new">Сначала новые</option> <option value="old">Сначала старые</option> </select> </label> </div> </div> </div> <!-- СЕТКА КАРТОЧЕК ПУБЛИЧНЫХ ВОПРОСОВ --> <div class="public-grid"> <% chats.forEach(function(c) { const created = c.created_at; const createdIso = created ? created.toISOString() : ""; %> <article class="public-card" data-title="<%= c.title.toLowerCase() %>" data-created="<%= createdIso %>" > <a class="public-card-link" href="/public/<%= c.id %>" aria-label="Открыть публичный чат" > <h2 class="public-card-title"><%= c.title %></h2> <div class="public-card-meta"> <span> 📅 <%= created ? created.toLocaleDateString("ru-RU") : "" %> </span> <% if (c.updated_at) { %> <span class="public-card-updated"> Обновлён: <%= c.updated_at.toLocaleDateString("ru-RU") %> </span> <% } %> </div> <p class="public-card-excerpt"> Открыть диалог полностью </p> </a> </article> <% }) %> </div> <footer class="public-footer"> <p class="public-footer-note"> Хочешь попасть в этот список? Открой чат, включи тумблер «Отображать публично» — и он появится здесь. </p> </footer> <% } %> </div> </div> </section> </main> <script> document.addEventListener("DOMContentLoaded", () => { const searchInput = document.querySelector(".public-search-input"); const filterButtons = document.querySelectorAll(".public-filter-btn"); const sortSelect = document.querySelector(".public-sort-select"); const grid = document.querySelector(".public-grid"); const cards = grid ? Array.from(grid.querySelectorAll(".public-card")) : []; if (!grid || !cards.length) return; function parseCreated(card) { const iso = card.getAttribute("data-created") || ""; const ts = Date.parse(iso); return Number.isNaN(ts) ? 0 : ts; } function applyFilters() { const query = (searchInput?.value || "").toLowerCase().trim(); const activeBtn = document.querySelector(".public-filter-btn.is-active"); const range = activeBtn ? activeBtn.dataset.range : "all"; const sort = sortSelect ? sortSelect.value : "new"; const now = Date.now(); let minTs = null; if (range === "day") { minTs = now - 24 * 60 * 60 * 1000; } else if (range === "week") { minTs = now - 7 * 24 * 60 * 60 * 1000; } else if (range === "month") { minTs = now - 30 * 24 * 60 * 60 * 1000; } const visible = []; cards.forEach((card) => { const title = (card.getAttribute("data-title") || "").toLowerCase(); const createdTs = parseCreated(card); // фильтр по тексту if (query && !title.includes(query)) { card.style.display = "none"; return; } // фильтр по диапазону дат if (minTs !== null && createdTs && createdTs < minTs) { card.style.display = "none"; return; } card.style.display = ""; visible.push(card); }); // сортировка visible.sort((a, b) => { const ta = parseCreated(a); const tb = parseCreated(b); if (sort === "old") return ta - tb; return tb - ta; // new }); visible.forEach((card) => grid.appendChild(card)); } if (searchInput) { searchInput.addEventListener("input", () => { applyFilters(); }); } filterButtons.forEach((btn) => { btn.addEventListener("click", () => { filterButtons.forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); applyFilters(); }); }); if (sortSelect) { sortSelect.addEventListener("change", applyFilters); } applyFilters(); }); </script> <%- include("partials/footer") %> Содержимое файла "public-chat-view.ejs" (фрагмент): <%- include("partials/header") %> <main class="site-main"> <section class="section"> <div class="container public-chat-layout"> <!-- ОСНОВНАЯ КОЛОНКА --> <div class="public-chat-main"> <!-- Хлебные крошки --> <nav class="breadcrumbs"> <a href="/">Главная</a> <span>›</span> <a href="/public">Публичные вопросы</a> <span>›</span> <span class="breadcrumbs-current"><%= chat.title %></span> </nav> <!-- ВЫБРАННЫЙ ПУБЛИЧНЫЙ ЧАТ КАК ОТДЕЛЬНАЯ КАРТОЧКА --> <section class="public-chat-selected-block" aria-label="Выбранный публичный вопрос"> <header class="public-chat-selected-header"> <h1 class="chat-title"><%= chat.title %></h1> <div class="chat-header-meta-row"> <div class="chat-header-meta-left"> <% if (chat.created_at) { %> <div class="chat-date-pill"> <span class="chat-date-icon">📅</span> <span class="chat-date-text"> <%= chat.created_at.toLocaleString("ru-RU") %> </span> </div> <% } %> </div> </div> </header> <!-- ПАНЕЛЬ ДЕЙСТВИЙ ПО ОТДЕЛЬНОМУ ЧАТУ --> <div class="public-chat-actions"> <button type="button" class="btn-secondary" onclick="window.location.href='/public'" > ← К списку вопросов </button> <button type="button" class="btn-secondary" id="btn-copy-link" > 🔗 Скопировать ссылку </button> </div> <!-- ОСНОВНАЯ КАРТОЧКА ДИАЛОГА --> <div class="card chat-card public-chat-thread-card"> <div class="chat-main"> <!-- САМ ДИАЛОГ --> <div id="chat-window" class="chat-window"> <% if (history && history.length) { history.forEach(function(turn) { %> <div class="chat-turn"> <!-- Сообщение пользователя --> <div class="bubble bubble-user chat-user-bubble"> <% if (turn.attachments && turn.attachments.length) { %> <div class="chat-attachments"> <% turn.attachments.forEach(function(att) { const url = '/uploads/' + att.rel_path; %> <div class="chat-attachment-card"> <a href="<%= url %>" target="_blank" rel="noopener noreferrer" class="chat-attachment-link" > <% if (att.mime_type && att.mime_type.startsWith('image/')) { %> <img src="<%= url %>" alt="<%= att.filename %>" class="chat-attachment-thumb" /> <% } else { %> <div class="chat-attachment-icon">📄</div> <% } %> <div class="chat-attachment-name"><%= att.filename %></div> </a> </div> <% }) %> </div> <% } %> <div class="bubble-main user-history-md"> <%= turn.question %> </div> </div> <!-- Ответ ассистента --> <div class="bubble bubble-ai chat-ai-answer"> <div class="bubble-main ai-history-md"> <%= turn.answer %> </div> </div> </div> <% }) } else { %> <div class="chat-empty-placeholder"> В этом чате пока нет сообщений. </div> <% } %> </div> </div> </div> </section> </div> <!-- САЙДБАР С ДРУГИМИ ВОПРОСАМИ --> <aside class="public-chat-sidebar" aria-label="Ещё публичные вопросы"> <div class="card public-sidebar-card"> <h2 class="public-sidebar-title">Ещё публичные вопросы</h2> <% if (typeof otherChats === "undefined" || !otherChats || !otherChats.length) { %> <p class="public-sidebar-empty"> Больше публичных вопросов пока нет. </p> <% } else { %> <ul class="public-sidebar-list"> <% otherChats.forEach(function(c) { %> <li class="public-sidebar-item"> <a href="/public/<%= c.id %>"> <span class="public-sidebar-item-title"><%= c.title %></span> <% if (c.created_at) { %> <span class="public-sidebar-item-date"> <%= c.created_at.toLocaleDateString("ru-RU") %> </span> <% } %> </a> </li> <% }) %> </ul> <% } %> <a href="/public" class="public-sidebar-more-link"> ← Вернуться к списку </a> </div> </aside> </div> </section> </main> <script> document.addEventListener("DOMContentLoaded", () => { const copyBtn = document.getElementById("btn-copy-link"); if (copyBtn && navigator.clipboard) { copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(window.location.href); const prev = copyBtn.textContent; copyBtn.textContent = "Ссылка скопирована"; setTimeout(() => (copyBtn.textContent = prev), 1500); } catch (e) { console.error(e); alert("Не получилось скопировать ссылку :("); } }); } }); </script> <%- include("partials/footer") %> Содержимое файла "chat.ejs" (фрагмент): <%- include("partials/header") %> <main class="site-main"> <section class="section"> <div class="container"> <!-- ОСНОВНАЯ КАРТОЧКА --> <div class="card chat-card"> <div class="chat-layout"> <!-- Сайдбар --> <aside class="chat-sidebar"> <div class="chat-sidebar-header"> <div class="chat-sidebar-title">Чаты</div> <div class="chat-sidebar-header-actions"> <button type="button" class="btn-secondary btn-new-chat" id="btn-new-chat" > + Новый чат </button> <!-- НОВОЕ: кнопка-карандаш для режима выбора --> <button type="button" id="chat-edit-toggle" class="chat-edit-toggle-btn" title="Выбрать чаты для удаления" > ✏️ </button> <button type="button" id="chat-sidebar-toggle" class="chat-sidebar-toggle" title="Показать / скрыть список чатов" > ☰ </button> </div> </div> <!-- НОВОЕ: панель массовых действий с чатами --> <div class="chat-sidebar-bulk-actions" id="chat-bulk-actions" > <div class="bulk-actions-left"> <button type="button" class="icon-btn bulk-actions-cancel" id="chat-bulk-cancel" title="Выйти из режима выбора" > ✕ </button> <span class="bulk-actions-label"> Выбрано: <span id="chat-bulk-count">0</span> </span> </div> <div class="bulk-actions-right"> <button type="button" class="btn-link" id="chat-bulk-select-all" > Выбрать все </button> <button type="button" class="btn-link" id="chat-bulk-clear" > Снять выделение </button> <button type="button" class="btn btn-danger btn-xs" id="chat-bulk-delete" disabled > Удалить выбранные </button> </div> </div> <div class="chat-list" id="chat-list"> <% if (typeof chats !== "undefined" && chats && chats.length) { %> <% chats.forEach(function(c) { %> <div class="chat-list-item <%= c.id === currentChatId ? 'is-active' : '' %>" data-chat-id="<%= c.id %>" > <div class="chat-list-main"> <div class="chat-list-title"><%= c.title %></div> <div class="chat-list-meta"> <%= c.updated_at ? c.updated_at.toLocaleString('ru-RU') : '' %> </div> </div> <div class="chat-list-menu"> <button type="button" class="chat-list-menu-btn" title="Действия" > ⋯ </button> <div class="chat-list-menu-popup"> <button type="button" data-action="rename"> Переименовать </button> <button type="button" data-action="delete"> Удалить </button> </div> </div> </div> <% }) %> <% } else { %> <div class="chat-list-empty"> Пока нет чатов. Нажми «Новый чат». </div> <% } %> </div> </aside> <!-- Правая часть --> <div class="chat-main"> <div class="chat-header"> <div class="chat-header-left"> <!-- SEO/семантика: главный заголовок --> <h1 class="chat-title"> <%= (typeof currentChatTitle !== "undefined" && currentChatTitle) ? currentChatTitle : "Новый чат с ИИ" %> </h1> <div class="chat-header-meta"> <!-- НОВЫЙ ЧАТ: только кнопка "Публично" --> <div class="chat-header-meta-row chat-header-meta-row--new <%= currentChatId ? 'is-hidden' : '' %>"> <div class="chat-header-meta-right"> <button type="button" class="chat-public-toggle chat-public-toggle--new" data-initial-state="0" > <span class="chat-public-label">Публично</span> <span class="chat-public-switch"> <span class="chat-public-knob"></span> </span> </button> </div> </div> <!-- СУЩЕСТВУЮЩИЙ ЧАТ: дата, публично, переименовать, удалить --> <div class="chat-header-meta-row chat-header-meta-row--existing <%= currentChatId ? '' : 'is-hidden' %>"> <div class="chat-header-meta-left"> <div class="chat-date-pill"> <span class="chat-date-icon">📅</span> <span class="chat-date-text"> <%= currentChatCreatedAt ? currentChatCreatedAt.toLocaleString("ru-RU") : "" %> </span> </div> </div> <div class="chat-header-meta-right"> <% const isPublic = (typeof currentChatIsPublic !== "undefined" && currentChatIsPublic); %> <button type="button" class="chat-public-toggle chat-public-toggle--existing" data-chat-id="<%= currentChatId || '' %>" data-initial-state="<%= isPublic ? '1' : '0' %>" > <span class="chat-public-label">Публично</span> <span class="chat-public-switch <%= isPublic ? 'is-on' : '' %>"> <span class="chat-public-knob"></span> </span> </button> <button type="button" class="chat-header-action-btn" data-action="rename-current" data-chat-id="<%= currentChatId || '' %>" > ✏️ <span>Переименовать</span> </button> <button type="button" class="chat-header-action-btn" data-action="delete-current" data-chat-id="<%= currentChatId || '' %>" > 🗑 <span>Удалить</span> </button> </div> </div> </div> </div> </div> <% let defaultChatKey = null; if (typeof selectedModelKey !== "undefined" && selectedModelKey) { defaultChatKey = selectedModelKey; } else if (models && models.length) { defaultChatKey = models[0].model_key; } %> <div id="chat-window" class="chat-window" data-chat-id="<%= currentChatId ? currentChatId : '' %>" > <% if (history && history.length) { history.forEach(function(turn) { const rawQ = turn.question || ""; const marker = "[Пользователь прикрепил следующие файлы:"; const cutPos = rawQ.indexOf(marker); const safeQuestion = (cutPos === -1) ? rawQ : rawQ.slice(0, cutPos); %> <div class="chat-turn" data-log-id="<%= turn.id %>"> <div class="bubble bubble-user chat-user-bubble"> <% if (turn.attachments && turn.attachments.length) { %> <div class="chat-attachments"> <% turn.attachments.forEach(function(att) { const url = '/uploads/' + att.rel_path; %> <div class="chat-attachment-card"> <a href="<%= url %>" target="_blank" rel="noopener noreferrer" class="chat-attachment-link" data-filename="<%= att.filename %>" data-mime="<%= att.mime_type || '' %>" data-size="<%= att.size_bytes || 0 %>" > ...[фрагмент файла обрезан] Содержимое файла "header.ejs" (фрагмент): <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>AIChat — персональный ChatGPT на русском без VPN</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta property="og:type" content="website"> <meta property="og:title" content="AIChat — персональный ChatGPT на русском без VPN"> <meta property="og:description" content="Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом."> <meta property="og:url" content="https://aichat.ru/"> <meta property="og:image" content="https://aichat.ru/og-image.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="AIChat — персональный ChatGPT на русском"> <meta name="twitter:description" content="Доступ к ChatGPT без VPN для пользователей из России."> <!-- РАНЬШЕ ВСЕГО выставляем сохранённые тему и язык --> <script> (function() { try { var savedTheme = localStorage.getItem('theme') || 'dark'; var savedLang = localStorage.getItem('lang') || 'ru'; document.documentElement.dataset.theme = savedTheme; document.documentElement.dataset.lang = savedLang; } catch (e) { document.documentElement.dataset.theme = 'dark'; document.documentElement.dataset.lang = 'ru'; } })(); </script> <!-- Стили --> <link rel="stylesheet" href="/css/styles.css"> <!-- Светлая тема отдельным файлом --> <link rel="stylesheet" href="/css/style-white.css"> <link rel="canonical" href="https://aichat.ru/"> <link rel="icon" href="/favicon.ico" type="image/x-icon"> <!-- highlight.js / markdown как было --> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Organization", "name": "AIChat — персональный ChatGPT на русском без VPN", "url": "https://aichat.ru/", "logo": "https://aichat.ru/og-image.png", "sameAs": [ "https://t.me/your_channel", "https://vk.com/your_group" ] } </script> <script defer src="/js/main.js"></script> <script defer src="/js/main2.js"></script> <script defer src="/js/feature-image-edit.js"></script> <!-- Наш JS для шапки --> <script defer src="/js/header-ui.js"></script> </head> <body> <header class="site-header"> <div class="container header-inner"> <!-- ЛЕВАЯ ЧАСТЬ: ЛОГО --> <div class="header-left"> <a href="<%= isAuthenticated ? '/chat' : '/' %>" class="logo" data-i18n-skip="1" > <span class="logo-mark"> <span class="logo-mark-inner">AI</span> </span> <span class="logo-text"> <span class="logo-main">Chat</span> <span class="logo-sub">personal chatgpt</span> </span> </a> </div> <!-- ЦЕНТР: МЕНЮ (для ДЕСКТОПА) --> <div class="header-center"> <nav class="nav"> <a href="/public" class="nav-link" data-i18n-key="nav.public">Публичные чаты</a> <% if (isAuthenticated) { %> <a href="/chat" class="nav-link nav-link--primary" data-i18n-key="nav.myChats">Мои чаты</a> <% } %> </nav> </div> <!-- ПРАВАЯ ЧАСТЬ: логин/рег/выход + тема + гамбургер --> <div class="header-right"> <!-- Переключатель темы (ВСЕГДА виден, и на мобилке тоже) --> <button type="button" class="icon-btn-1 theme-toggle" id="theme-toggle" aria-label="Переключить тему" > <span class="theme-icon theme-icon-sun">☀</span> <span class="theme-icon theme-icon-moon">🌙</span> </button> <% if (!isAuthenticated) { %> <a href="/login" class="nav-link" data-i18n-key="nav.login">Вход</a> <a href="/register" class="nav-link nav-link--primary nav-cta" data-i18n-key="nav.signup"> Регистрация </a> <% } %> <!-- ГАМБУРГЕР: на десктопе скрыт, на мобилке СПРАВА ОТ ТЕМЫ --> <button type="button" class="header-burger" id="header-burger" aria-label="Открыть меню" aria-expanded="false" > <span></span> <span></span> <span></span> </button> <% if (isAuthenticated) { %> <form action="/logout" method="post" class="header-logout-form"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <button type="submit" class="nav-link nav-link--ghost" data-i18n-key="nav.logout"> Выйти </button> </form> <% } %> </div> </div> <!-- МОБИЛЬНОЕ ВЫПАДАЮЩЕЕ МЕНЮ ГАМБУРГЕРА --> <div class="container mobile-menu" id="mobile-menu"> <nav class="mobile-nav"> <a href="/public" class="nav-link" data-i18n-key="nav.public">Публичные чаты</a> <% if (isAuthenticated) { %> <a href="/chat" class="nav-link nav-link--primary" data-i18n-key="nav.myChats">Мои чаты</a> <form action="/logout" method="post" class="mobile-logout-form"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <button type="submit" class="nav-link nav-link--ghost" data-i18n-key="nav.logout"> Выйти </button> </form> <% } else { %> <a href="/login" class="nav-link" data-i18n-key="nav.login">Вход</a> <a href="/register" class="nav-link nav-link--primary nav-cta" data-i18n-key="nav.signup"> Регистрация </a> <% } %> </nav> </div> </header> ]
Проблема как раз в `partials/header.ejs`: там `<title>`, OG-теги и `<link rel="canonical">` захардкожены, а этот хедер подключается на всех страницах → для поисковиков все страницы как будто одна и та же. Нужно сделать хедер «умным», а в шаблонах страниц прокидывать нужный `title`/description/canonical. --- ## 1. Делаем header.ejs динамическим Сейчас (фрагмент): ```ejs <title>AIChat — персональный ChatGPT на русском без VPN</title> <meta property="og:title" content="AIChat — персональный ChatGPT на русском без VPN"> <meta property="og:description" content="Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом."> <meta property="og:url" content="https://aichat.ru/"> <meta name="twitter:title" content="AIChat — персональный ChatGPT на русском"> <meta name="twitter:description" content="Доступ к ChatGPT без VPN для пользователей из России."> <link rel="canonical" href="https://aichat.ru/"> ``` Предлагаю заменить на вариант с переменными и дефолтами: ```ejs <title><%= pageTitle || 'AIChat — персональный ChatGPT на русском без VPN' %></title> <meta name="description" content="<%= metaDescription || 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.' %>"> <meta property="og:type" content="website"> <meta property="og:title" content="<%= ogTitle || pageTitle || 'AIChat — персональный ChatGPT на русском без VPN' %>"> <meta property="og:description" content="<%= ogDescription || metaDescription || 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.' %>"> <meta property="og:url" content="<%= ogUrl || canonicalUrl || 'https://aichat.ru/' %>"> <meta property="og:image" content="https://aichat.ru/og-image.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="<%= twitterTitle || pageTitle || 'AIChat — персональный ChatGPT на русском' %>"> <meta name="twitter:description" content="<%= twitterDescription || metaDescription || 'Доступ к ChatGPT без VPN для пользователей из России.' %>"> <link rel="canonical" href="<%= canonicalUrl || 'https://aichat.ru/' %>"> ``` Остальная часть `header.ejs` может остаться как есть. Если на бэкенде уже пробрасываются какие-то локали для SEO — можно ими пользоваться вместо `pageTitle`/`metaDescription`. В доступных файлах я этого не вижу, поэтому дальше показываю вариант, когда мы задаём их прямо в EJS. --- ## 2. Главная (`index.ejs`) Сейчас файл начинается сразу с: ```ejs <%- include("partials/header") %> ``` Чтобы главная была как сейчас, можно явно задать переменные перед подключением хедера: ```ejs <% pageTitle = 'AIChat — персональный ChatGPT на русском без VPN'; metaDescription = 'Быстрый и удобный ChatGPT на русском языке. Работает без VPN, доступные тарифы, помощь с текстами, кодом и аналитикой.'; canonicalUrl = 'https://aichat.ru/'; ogTitle = pageTitle; ogDescription = metaDescription; %> <%- include("partials/header") %> ``` Дальше — твой существующий контент. --- ## 3. Список публичных чатов (`public-chats.ejs`) Сейчас тоже сразу: ```ejs <%- include("partials/header") %> ``` Меняем верх файла на: ```ejs <% pageTitle = 'Публичные вопросы к ChatGPT на русском — AIChat'; metaDescription = 'Лента публичных вопросов пользователей к ИИ: реальные запросы и ответы ChatGPT на русском языке.'; canonicalUrl = 'https://aichat.ru/public'; ogTitle = pageTitle; ogDescription = metaDescription; %> <%- include("partials/header") %> ``` Остальной шаблон можно не трогать. --- ## 4. Страница отдельного публичного чата (`public-chat-view.ejs`) Сейчас: ```ejs <%- include("partials/header") %> <main class="site-main"> ... ``` Меняем начало файла на: ```ejs <% const plainTitle = chat.title || 'Публичный вопрос к ИИ'; pageTitle = plainTitle + ' — ответ ChatGPT на русском | AIChat'; metaDescription = 'Ответ нейросети на вопрос: ' + plainTitle; canonicalUrl = 'https://aichat.ru/public/' + chat.id; ogTitle = pageTitle; ogDescription = metaDescription; ogUrl = canonicalUrl; %> <%- include("partials/header") %> <main class="site-main"> ... ``` Плюсы: - У каждого публичного чата будет уникальный `<title>` и `<meta description>`, завязанные на `chat.title`. - Канонический URL указывает на конкретную страницу чата. - OG/Twitter-превью в соцсетях тоже будут с заголовком вопроса. --- ## 5. Страница личных чатов (`chat.ejs`) Эта страница, скорее, для залогиненных и не так важна для индексации, но чтобы не плодить дубликаты, можно тоже задать свои метаданные. Сейчас в фрагменте: ```ejs <%- include("partials/header") %> <main class="site-main"> <section class="section"> ... ``` Можно сделать так: ```ejs <% pageTitle = 'Личный чат с ChatGPT на русском — AIChat'; metaDescription = 'Личный кабинет с историей диалогов с нейросетью, выбором моделей и возможностью сделать чат публичным.'; canonicalUrl = 'https://aichat.ru/chat'; ogTitle = pageTitle; ogDescription = metaDescription; %> <%- include("partials/header") %> <main class="site-main"> ... ``` --- ## 6. Что ещё стоит учесть 1. **Бэкенд (контроллеры)** В шаблонах я использую простую схему `pageTitle = ...` прямо в EJS. Более «чистый» вариант — задавать `pageTitle`, `metaDescription`, `canonicalUrl` в `res.render('...', { pageTitle: '...', ... })`. В доступных файлах таких вызовов нет, поэтому я не привожу конкретный код контроллеров. 2. **Длина тайтлов и описаний** Для Яндекса/Google желательно держать `<title>` до ~60–70 символов, description до ~140–160. Если `chat.title` может быть очень длинным, можно обрезать его: ```ejs const shortTitle = (chat.title || 'Публичный вопрос к ИИ').slice(0, 80); pageTitle = shortTitle + ' — ответ ChatGPT на русском | AIChat'; ``` 3. **OG:image** Сейчас в `header.ejs` везде одна и та же картинка — это норм, если нет задачи генерировать отдельные превью для каждого чата. --- Если хочешь, могу подсказать конкретные формулировки `<title>`/description под разные типы вопросов (код, маркетинг, учеба), но базовое решение — сделать `header.ejs` динамическим и передавать SEO-переменные из шаблонов — уже снимет проблему одинаковых тайтлов на всех страницах.
ну вот блять кусок, нормально пиши как есть сука!!! <meta charset="UTF-8"> <title>AIChat — персональный ChatGPT на русском без VPN</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta property="og:type" content="website"> <meta property="og:title" content="AIChat — персональный ChatGPT на русском без VPN"> <meta property="og:description" content="Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом."> <meta property="og:url" content="https://aichat.ru/"> <meta property="og:image" content="https://aichat.ru/og-image.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="AIChat — персональный ChatGPT на русском"> <meta name="twitter:description" content="Доступ к ChatGPT без VPN для пользователей из России."> На что его заменить???
## Ответ: Ок, даю **конкретно**, без воды, **вставляй как есть**. Это **замена твоего блока** 1-в-1, чтобы тайтл и мета-теги стали **динамическими через переменные** (`pageTitle`, `metaDescription`, `canonicalUrl` и т.п.). --- # 🔥 **Вставь вот ЭТО вместо твоего куска** ```ejs <meta charset="UTF-8"> <title><%= pageTitle || 'AIChat — персональный ChatGPT на русском без VPN' %></title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="<%= metaDescription || 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.' %>"> <meta property="og:type" content="website"> <meta property="og:title" content="<%= ogTitle || pageTitle || 'AIChat — персональный ChatGPT на русском без VPN' %>"> <meta property="og:description" content="<%= ogDescription || metaDescription || 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.' %>"> <meta property="og:url" content="<%= ogUrl || canonicalUrl || 'https://aichat.ru/' %>"> <meta property="og:image" content="https://aichat.ru/og-image.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="<%= twitterTitle || pageTitle || 'AIChat — персональный ChatGPT на русском' %>"> <meta name="twitter:description" content="<%= twitterDescription || metaDescription || 'Доступ к ChatGPT без VPN для пользователей из России.' %>"> <link rel="canonical" href="<%= canonicalUrl || 'https://aichat.ru/' %>"> ``` --- # ❗Что это тебе даёт Теперь: - на **любую страницу** ты можешь задать свой `<title>` - свой `<meta description>` - свой canonical - отдельные OG / Twitter поля И поисковики перестанут видеть **все страницы одинаковыми**. --- # 🔥 Пример как задавать на странице Вверху `public-chat-view.ejs`: ```ejs <% pageTitle = chat.title + ' — ChatGPT ответ'; metaDescription = 'Публичный вопрос: ' + chat.title; canonicalUrl = 'https://aichat.ru/public/' + chat.id; %> <%- include('partials/header') %> ``` --- Если хочешь, дам готовые блоки для **публичных чатов**, **главной**, **лентой публичных**, **личных чатов** — просто скажи.
ReferenceError: C:\VSCode_programm\ChatGPT\chatgpt-site\views\chat.ejs:8 6| ogDescription = metaDescription; 7| %> >> 8| <%- include("partials/header") %> 9| 10| <main class="site-main"> 11| <section class="section"> C:\VSCode_programm\ChatGPT\chatgpt-site\views\partials\header.ejs:14 12| <meta property="og:title" content="<%= ogTitle || pageTitle || 'AIChat — персональный ChatGPT на русском без VPN' %>"> 13| <meta property="og:description" content="<%= ogDescription || metaDescription || 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.' %>"> >> 14| <meta property="og:url" content="<%= ogUrl || canonicalUrl || 'https://aichat.ru/' %>"> 15| <meta property="og:image" content="https://aichat.ru/og-image.png"> 16| 17| <meta name="twitter:card" content="summary_large_image"> ogUrl is not defined at eval ("C:\\VSCode_programm\\ChatGPT\\chatgpt-site\\views\\partials\\header.ejs":24:26) at header (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\ejs\lib\ejs.js:703:17) at include (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\ejs\lib\ejs.js:701:39) at eval ("C:\\VSCode_programm\\ChatGPT\\chatgpt-site\\views\\chat.ejs":20:17) at chat (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\ejs\lib\ejs.js:703:17) at tryHandleCache (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\ejs\lib\ejs.js:274:36) at exports.renderFile [as engine] (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\ejs\lib\ejs.js:491:10) at View.render (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\express\lib\view.js:135:8) at tryRender (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\express\lib\application.js:657:10) at Function.render (C:\VSCode_programm\ChatGPT\chatgpt-site\node_modules\express\lib\application.js:609:3)
Ошибка логичная: в `header.ejs` ты используешь переменную `ogUrl`, а **ни в одном шаблоне её не задаёшь** → EJS падает с `ReferenceError`. Чтобы не ловить такие ошибки вообще, сделай шапку самодостаточной: все SEO-переменные внутри неё сначала приводим к дефолтам, а потом используем. 🎯 **Скопируй и замени весь свой блок `<meta ...>` в `partials/header.ejs` на вот это:** ```ejs <% const _pageTitle = typeof pageTitle !== 'undefined' ? pageTitle : 'AIChat — персональный ChatGPT на русском без VPN'; const _metaDescription = typeof metaDescription !== 'undefined' ? metaDescription : 'Быстрый и удобный доступ к ChatGPT в России. Работает без VPN, доступные тарифы, помощь с текстами и кодом.'; const _canonicalUrl = typeof canonicalUrl !== 'undefined' ? canonicalUrl : 'https://aichat.ru/'; const _ogTitle = typeof ogTitle !== 'undefined' ? ogTitle : _pageTitle; const _ogDescription = typeof ogDescription !== 'undefined' ? ogDescription : _metaDescription; const _twitterTitle = typeof twitterTitle !== 'undefined' ? twitterTitle : _pageTitle; const _twitterDescription = typeof twitterDescription !== 'undefined' ? twitterDescription : _metaDescription; %> <meta charset="UTF-8"> <title><%= _pageTitle %></title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="<%= _metaDescription %>"> <meta property="og:type" content="website"> <meta property="og:title" content="<%= _ogTitle %>"> <meta property="og:description" content="<%= _ogDescription %>"> <meta property="og:url" content="<%= _canonicalUrl %>"> <meta property="og:image" content="https://aichat.ru/og-image.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="<%= _twitterTitle %>"> <meta name="twitter:description" content="<%= _twitterDescription %>"> <link rel="canonical" href="<%= _canonicalUrl %>"> ``` 📌 Что изменилось: - Больше **нет `ogUrl` вообще** → не может упасть. - Если ты **ничего не задаёшь** в конкретной странице — используются дефолты (как было изначально). - Если вверху страницы пишешь, например, в `chat.ejs`: ```ejs <% pageTitle = 'Личный чат с ChatGPT на русском — AIChat'; metaDescription = 'Личный кабинет с историей диалогов с нейросетью.'; canonicalUrl = 'https://aichat.ru/chat'; %> <%- include("partials/header") %> ``` — то шапка автоматом подставит эти значения. Если после этого ещё где-то что-то свалится — кидай стек и кусок файла, поправим точечно.