Consolidate Organization schema, add ItemList/Offer markup, expand FAQ
- Merge the duplicate Organization/LocalBusiness JSON-LD blocks (home's LocalBusiness + o-kompanii's Organization) into one shared getOrganizationSchema() call rendered on every page. Deliberately NOT wired through the root layout: fetches there get frozen into the static shell at build time (backend unreachable during image build) and never revalidate, silently serving stale data to crawlers - each page calls it directly instead, where revalidation is proven to work. - Add optional ItemList schema on /uslugi and Offer/OfferCatalog schema on /aktsii; entity pages (/uslugi/[slug] Service, /blog/[slug] Article) reference the organization via @id instead of duplicating it. - Add 8 more real FAQ entries (pricing model, contract terms, onboarding time, hybrid IT staff setup, supported stacks, backups, 152-FZ compliance, escalation) and give every FAQ its own slug and dedicated /faq/[slug] page with its own title/H1/FAQPage schema, modeled on the one-topic-per-page pattern (h/t remontka.pro). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
1 parent
f53c8b1ed7
commit
1e4f97030a
19 files changed
+271
-69
No files matched your search
@@ -0,0 +1,48 @@
|
||||
-- Добавляет slug к FAQ (для отдельных страниц под каждый вопрос - по образцу
|
||||
-- сайтов с базой знаний, где один вопрос/проблема = один адрес, а не только
|
||||
-- пункт в общем аккордеоне) и добавляет более полный набор реальных вопросов
|
||||
-- про условия работы, а не только про общий сервис.
|
||||
-- Автоматически применяется при следующем запуске бэкенда.
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE faqs ADD COLUMN IF NOT EXISTS slug TEXT;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_faqs_slug ON faqs(slug) WHERE slug IS NOT NULL;
|
||||
|
||||
UPDATE faqs SET slug = 'vremya-reakcii-na-zayavki' WHERE question = 'Как быстро вы реагируете на заявки?' AND slug IS NULL;
|
||||
UPDATE faqs SET slug = 'udalennaya-podderzhka-po-rossii' WHERE question = 'Работаете ли вы полностью удалённо?' AND slug IS NULL;
|
||||
UPDATE faqs SET slug = 'chto-vhodit-v-bazovyy-tarif' WHERE question = 'Что входит в базовый тариф IT-аутсорсинга?' AND slug IS NULL;
|
||||
UPDATE faqs SET slug = 'kak-oplachivat-uslugi' WHERE question = 'Как происходит оплата услуг?' AND slug IS NULL;
|
||||
UPDATE faqs SET slug = 'razovaya-usluga-bez-abonementa' WHERE question = 'Можно ли заказать разовую услугу без абонемента?' AND slug IS NULL;
|
||||
UPDATE faqs SET slug = 'konfidencialnost-dannyh' WHERE question = 'Как обеспечивается конфиденциальность данных?' AND slug IS NULL;
|
||||
|
||||
INSERT INTO faqs (question, answer, slug, sort_order)
|
||||
SELECT v.question, v.answer, v.slug, (SELECT COALESCE(MAX(sort_order), -1) + 1 FROM faqs) + v.rn - 1
|
||||
FROM (VALUES
|
||||
('Как рассчитывается стоимость услуг?',
|
||||
'Стоимость абонентского обслуживания зависит от количества рабочих мест, графика поддержки (5/2 или 24/7) и состава инфраструктуры. Разовые работы (настройка сервера, аудит, разработка) оцениваются по объёму задачи после короткого созвона или Discovery-воркшопа. Точную сумму называем после короткого аудита - до этого момента ориентируемся на прайс-лист.',
|
||||
'kak-rasschityvaetsya-stoimost', 1),
|
||||
('Можно ли расторгнуть договор на обслуживание?',
|
||||
'Да, договор на абонентское обслуживание можно расторгнуть, уведомив нас заранее (обычно за 30 дней) - жёсткой привязки на длительный срок нет, кроме случаев, когда клиент сам выбрал тариф с фиксированным сроком ради скидки (см. раздел «Акции»). Все условия фиксируются в договоре до начала работ.',
|
||||
'kak-rastorgnut-dogovor', 2),
|
||||
('Сколько занимает подключение к обслуживанию?',
|
||||
'Обычно от 1 до 5 рабочих дней с момента подписания договора: сначала проводим аудит текущей инфраструктуры, получаем доступы, документируем текущее состояние - и только после этого переходим на плановое обслуживание. Для срочных случаев (например, уже произошёл инцидент) можем начать работу быстрее.',
|
||||
'skolko-zanimaet-podklyuchenie', 3),
|
||||
('Что если у нас уже есть свой системный администратор?',
|
||||
'Работаем и в гибридном формате: берём на себя часть инфраструктуры (например, кибербезопасность или почтовые серверы), пока штатный специалист занимается остальным, либо выступаем вторым уровнем поддержки на случай отпуска или больничного. Разграничение зон ответственности фиксируется в договоре.',
|
||||
'rabota-so-svoim-sisadminom', 4),
|
||||
('С какими операционными системами и СУБД вы работаете?',
|
||||
'Администрируем инфраструктуру на Linux и Windows Server, из СУБД - чаще всего PostgreSQL и MySQL, но беремся и за другие популярные СУБД по запросу. Перед стартом уточняем состав вашего стека на аудите.',
|
||||
's-kakimi-sistemami-rabotaete', 5),
|
||||
('Как обеспечивается резервное копирование данных?',
|
||||
'Настраиваем регулярные автоматические бэкапы, изолированные от основной инфраструктуры (чтобы шифровальщик или сбой сервера не уничтожил и рабочие данные, и резервные копии одновременно), и периодически проверяем, что из бэкапа реально можно восстановиться - не только что он создаётся.',
|
||||
'kak-obespechivaetsya-rezervnoe-kopirovanie', 6),
|
||||
('Работаете ли вы с требованиями 152-ФЗ о персональных данных?',
|
||||
'Да, при настройке инфраструктуры учитываем требования 152-ФЗ к обработке и хранению персональных данных: разграничение доступов, шифрование каналов, локализация данных на серверах в РФ при необходимости. Полный аудит на соответствие 152-ФЗ - по отдельному запросу.',
|
||||
'rabota-s-152-fz', 7),
|
||||
('Что происходит, если проблему не удаётся решить сразу?',
|
||||
'Если проблема не решается на первом уровне поддержки, подключаем более опытного специалиста - это часть стандартного процесса, без дополнительной оплаты для клиентов на абонентском обслуживании. О ходе решения сложных инцидентов сообщаем в личном кабинете или напрямую.',
|
||||
'eskalaciya-slozhnyh-problem', 8)
|
||||
) AS v(question, answer, slug, rn)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM faqs WHERE slug = v.slug);
|
||||
|
||||
COMMIT;
|
||||
@@ -42,11 +42,20 @@ router.get('/cases', async (req, res) => {
|
||||
|
||||
router.get('/faqs', async (req, res) => {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, question, answer FROM faqs WHERE active = true ORDER BY sort_order, id'
|
||||
'SELECT id, question, answer, slug FROM faqs WHERE active = true ORDER BY sort_order, id'
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.get('/faqs/:slug', async (req, res) => {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, question, answer, slug FROM faqs WHERE slug = $1 AND active = true',
|
||||
[req.params.slug]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
|
||||
router.get('/blog', async (req, res) => {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, slug, title, excerpt, published_at FROM blog_posts WHERE published = true ORDER BY published_at DESC'
|
||||
|
||||
Reference in new issue
Block a user