У меня есть проблема, что во всех частях сайта тайтл один и тот же "AIChat —...
у меня есть проблема, что во всех частях сайта тайтл один и тот же "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="Например: «Поменяй текст на вывеске на "Открыто" и убери лишние объекты на заднем плане»"
></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") %>
```
— то шапка автоматом подставит эти значения.
Если после этого ещё где-то что-то свалится — кидай стек и кусок файла, поправим точечно.