seo: GA4, breadcrumbs JSON-LD, IndexNow post-deploy, llms.txt
- Add GA4 (G-C9J0D8FFH3) to root layout alongside Yandex.Metrika - Add BreadcrumbList JSON-LD schema to all inner pages - Add scripts/indexnow.mjs — submits 30 URLs to IndexNow + Yandex on deploy - Add indexnow to postdeploy step in package.json - Update llms.txt with all 8 services and new pages (about/clients/partners) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"deploy": "BUILD_DIR=../public_html next build"
|
"deploy": "BUILD_DIR=../public_html next build && node scripts/indexnow.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
|
|||||||
@@ -12,10 +12,25 @@
|
|||||||
- **IT-аутсорсинг** — полное обслуживание IT-инфраструктуры: серверы, сети, рабочие станции. Поддержка 24/7, удалённое и выездное обслуживание, фиксированная стоимость.
|
- **IT-аутсорсинг** — полное обслуживание IT-инфраструктуры: серверы, сети, рабочие станции. Поддержка 24/7, удалённое и выездное обслуживание, фиксированная стоимость.
|
||||||
- **Кибербезопасность** — защита корпоративных данных, аудит безопасности, настройка межсетевых экранов и VPN, защита от утечек данных.
|
- **Кибербезопасность** — защита корпоративных данных, аудит безопасности, настройка межсетевых экранов и VPN, защита от утечек данных.
|
||||||
- **Техническая поддержка** — help desk, управление заявками, обучение пользователей. SLA от 15 минут.
|
- **Техническая поддержка** — help desk, управление заявками, обучение пользователей. SLA от 15 минут.
|
||||||
|
- **Видеонаблюдение** — проектирование и монтаж систем видеонаблюдения, IP-камеры, NVR, облачное хранение, удалённый доступ.
|
||||||
|
- **Сетевая инфраструктура** — проектирование, монтаж и обслуживание корпоративных сетей, Wi-Fi, VPN, управляемые коммутаторы.
|
||||||
|
- **Серверная инфраструктура** — поставка, настройка и обслуживание серверов, виртуализация, резервное копирование.
|
||||||
|
- **IP-телефония** — корпоративные АТС, SIP-телефония, интеграция с CRM, запись звонков.
|
||||||
|
- **Облачные решения** — перенос инфраструктуры в облако, Microsoft 365, резервное копирование в облаке.
|
||||||
|
|
||||||
|
## Страницы
|
||||||
|
|
||||||
|
- Главная: https://sag24.ru/ru/
|
||||||
|
- Услуги: https://sag24.ru/ru/uslugi/
|
||||||
|
- О компании: https://sag24.ru/ru/about/
|
||||||
|
- Клиенты: https://sag24.ru/ru/clients/
|
||||||
|
- Партнёры: https://sag24.ru/ru/partners/
|
||||||
|
- FAQ: https://sag24.ru/ru/faq/
|
||||||
|
- Контакты: https://sag24.ru/ru/kontakty/
|
||||||
|
|
||||||
## Контакты
|
## Контакты
|
||||||
|
|
||||||
- Телефон: +7 495 363-74-76
|
- Телефон: +7 495 363-74-76, +7 495 363-73-35, +7 909 945-44-56
|
||||||
- Email: info@sag24.ru
|
- Email: info@sag24.ru
|
||||||
- Адрес: Пушкино, Московская область
|
- Адрес: Пушкино, пр-кт Московский, д. 38/14, Московская область, 141207
|
||||||
- Сайт: https://sag24.ru
|
- Сайт: https://sag24.ru
|
||||||
|
|||||||
68
scripts/indexnow.mjs
Normal file
68
scripts/indexnow.mjs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// IndexNow — уведомляем поисковики об обновлении сайта после деплоя
|
||||||
|
// https://www.indexnow.org/
|
||||||
|
|
||||||
|
const KEY = '40c65b722891b81d944f2c3fea6cab95'
|
||||||
|
const HOST = 'sag24.ru'
|
||||||
|
|
||||||
|
const URLS = [
|
||||||
|
'https://sag24.ru/ru/',
|
||||||
|
'https://sag24.ru/en/',
|
||||||
|
'https://sag24.ru/ru/about/',
|
||||||
|
'https://sag24.ru/en/about/',
|
||||||
|
'https://sag24.ru/ru/clients/',
|
||||||
|
'https://sag24.ru/en/clients/',
|
||||||
|
'https://sag24.ru/ru/partners/',
|
||||||
|
'https://sag24.ru/en/partners/',
|
||||||
|
'https://sag24.ru/ru/uslugi/',
|
||||||
|
'https://sag24.ru/en/uslugi/',
|
||||||
|
'https://sag24.ru/ru/uslugi/it-autsorsing/',
|
||||||
|
'https://sag24.ru/en/uslugi/it-autsorsing/',
|
||||||
|
'https://sag24.ru/ru/uslugi/kiberbezopasnost/',
|
||||||
|
'https://sag24.ru/en/uslugi/kiberbezopasnost/',
|
||||||
|
'https://sag24.ru/ru/uslugi/tehpodderzhka/',
|
||||||
|
'https://sag24.ru/en/uslugi/tehpodderzhka/',
|
||||||
|
'https://sag24.ru/ru/uslugi/videonablyudenie/',
|
||||||
|
'https://sag24.ru/en/uslugi/videonablyudenie/',
|
||||||
|
'https://sag24.ru/ru/uslugi/seti/',
|
||||||
|
'https://sag24.ru/en/uslugi/seti/',
|
||||||
|
'https://sag24.ru/ru/uslugi/servery/',
|
||||||
|
'https://sag24.ru/en/uslugi/servery/',
|
||||||
|
'https://sag24.ru/ru/uslugi/telefoniya/',
|
||||||
|
'https://sag24.ru/en/uslugi/telefoniya/',
|
||||||
|
'https://sag24.ru/ru/uslugi/oblako/',
|
||||||
|
'https://sag24.ru/en/uslugi/oblako/',
|
||||||
|
'https://sag24.ru/ru/faq/',
|
||||||
|
'https://sag24.ru/en/faq/',
|
||||||
|
'https://sag24.ru/ru/kontakty/',
|
||||||
|
'https://sag24.ru/en/kontakty/',
|
||||||
|
]
|
||||||
|
|
||||||
|
const ENDPOINTS = [
|
||||||
|
'https://api.indexnow.org/indexnow',
|
||||||
|
'https://yandex.com/indexnow',
|
||||||
|
]
|
||||||
|
|
||||||
|
async function submit(endpoint) {
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
host: HOST,
|
||||||
|
key: KEY,
|
||||||
|
keyLocation: `https://${HOST}/${KEY}.txt`,
|
||||||
|
urlList: URLS,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return res.status
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`IndexNow: submitting ${URLS.length} URLs...`)
|
||||||
|
const results = await Promise.allSettled(ENDPOINTS.map(async ep => {
|
||||||
|
const status = await submit(ep)
|
||||||
|
console.log(` ${ep} → ${status}`)
|
||||||
|
return status
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ok = results.every(r => r.status === 'fulfilled' && (r.value === 200 || r.value === 202))
|
||||||
|
console.log(ok ? 'IndexNow: done.' : 'IndexNow: some errors (non-critical).')
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import AboutSection from '@/components/sections/AboutSection'
|
import AboutSection from '@/components/sections/AboutSection'
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -37,9 +38,14 @@ export default async function AboutPage({ params }: { params: Promise<{ lang: st
|
|||||||
const { lang: langStr } = await params
|
const { lang: langStr } = await params
|
||||||
const lang = langStr as Locale
|
const lang = langStr as Locale
|
||||||
const d = getDictionary(lang)
|
const d = getDictionary(lang)
|
||||||
|
const isRu = lang === 'ru'
|
||||||
return (
|
return (
|
||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
<AboutSection d={d} standalone />
|
<AboutSection d={d} standalone />
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
|
{ name: isRu ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
|
{ name: isRu ? 'О компании' : 'About', url: `https://sag24.ru/${lang}/about/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import ClientsSection from '@/components/sections/ClientsSection'
|
import ClientsSection from '@/components/sections/ClientsSection'
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -37,9 +38,14 @@ export default async function ClientsPage({ params }: { params: Promise<{ lang:
|
|||||||
const { lang: langStr } = await params
|
const { lang: langStr } = await params
|
||||||
const lang = langStr as Locale
|
const lang = langStr as Locale
|
||||||
const d = getDictionary(lang)
|
const d = getDictionary(lang)
|
||||||
|
const isRu = lang === 'ru'
|
||||||
return (
|
return (
|
||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
<ClientsSection d={d} standalone />
|
<ClientsSection d={d} standalone />
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
|
{ name: isRu ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
|
{ name: isRu ? 'Клиенты' : 'Clients', url: `https://sag24.ru/${lang}/clients/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import FaqSection from '@/components/sections/FaqSection'
|
import FaqSection from '@/components/sections/FaqSection'
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -49,13 +50,15 @@ export default async function FaqPage({ params }: { params: Promise<{ lang: stri
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRu = lang === 'ru'
|
||||||
return (
|
return (
|
||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
<FaqSection d={d} standalone />
|
<FaqSection d={d} standalone />
|
||||||
<script
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }} />
|
||||||
type="application/ld+json"
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
|
{ name: isRu ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
/>
|
{ name: 'FAQ', url: `https://sag24.ru/${lang}/faq/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import ContactForm from '@/components/ui/ContactForm'
|
import ContactForm from '@/components/ui/ContactForm'
|
||||||
import { Phone, Mail, MapPin } from 'lucide-react'
|
import { Phone, Mail, MapPin } from 'lucide-react'
|
||||||
|
|
||||||
@@ -128,10 +129,11 @@ export default async function KontaktyPage({ params }: { params: Promise<{ lang:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessSchema) }} />
|
||||||
type="application/ld+json"
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessSchema) }}
|
{ name: lang === 'ru' ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
/>
|
{ name: lang === 'ru' ? 'Контакты' : 'Contact', url: `https://sag24.ru/${lang}/kontakty/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import PartnersSection from '@/components/sections/PartnersSection'
|
import PartnersSection from '@/components/sections/PartnersSection'
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -37,9 +38,14 @@ export default async function PartnersPage({ params }: { params: Promise<{ lang:
|
|||||||
const { lang: langStr } = await params
|
const { lang: langStr } = await params
|
||||||
const lang = langStr as Locale
|
const lang = langStr as Locale
|
||||||
const d = getDictionary(lang)
|
const d = getDictionary(lang)
|
||||||
|
const isRu = lang === 'ru'
|
||||||
return (
|
return (
|
||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
<PartnersSection d={d} standalone />
|
<PartnersSection d={d} standalone />
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
|
{ name: isRu ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
|
{ name: isRu ? 'Партнёры' : 'Partners', url: `https://sag24.ru/${lang}/partners/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Link from 'next/link'
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
import { SERVICE_SLUGS } from '@/lib/services'
|
import { SERVICE_SLUGS } from '@/lib/services'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import { Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud, CheckCircle2, ChevronRight } from 'lucide-react'
|
import { Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud, CheckCircle2, ChevronRight } from 'lucide-react'
|
||||||
|
|
||||||
const ICONS = [Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud]
|
const ICONS = [Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud]
|
||||||
@@ -109,6 +110,14 @@ export default async function ServicePage({ params }: { params: Promise<{ lang:
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
|
{ name: lang === 'ru' ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
|
{ name: lang === 'ru' ? 'Услуги' : 'Services', url: `https://sag24.ru/${lang}/uslugi/` },
|
||||||
|
{ name: svc.title, url: `https://sag24.ru/${lang}/uslugi/${slug}/` },
|
||||||
|
])) }}
|
||||||
|
/>
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
import { getDictionary, LOCALES, type Locale } from '@/lib/i18n'
|
||||||
|
import { breadcrumbSchema } from '@/lib/breadcrumbs'
|
||||||
import { Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud } from 'lucide-react'
|
import { Server, Shield, Headphones, Camera, Network, HardDrive, Phone, Cloud } from 'lucide-react'
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -78,6 +79,10 @@ export default async function UslugiPage({ params }: { params: Promise<{ lang: s
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema([
|
||||||
|
{ name: lang === 'ru' ? 'Главная' : 'Home', url: `https://sag24.ru/${lang}/` },
|
||||||
|
{ name: lang === 'ru' ? 'Услуги' : 'Services', url: `https://sag24.ru/${lang}/uslugi/` },
|
||||||
|
])) }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" strategy="lazyOnload" />
|
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" strategy="lazyOnload" />
|
||||||
|
{/* Google Analytics GA4 */}
|
||||||
|
<Script src="https://www.googletagmanager.com/gtag/js?id=G-C9J0D8FFH3" strategy="afterInteractive" />
|
||||||
|
<Script id="ga4" strategy="afterInteractive">{`
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', 'G-C9J0D8FFH3');
|
||||||
|
`}</Script>
|
||||||
{/* Yandex.Metrika */}
|
{/* Yandex.Metrika */}
|
||||||
<Script id="ym" strategy="afterInteractive">{`
|
<Script id="ym" strategy="afterInteractive">{`
|
||||||
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
|||||||
12
src/lib/breadcrumbs.ts
Normal file
12
src/lib/breadcrumbs.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export function breadcrumbSchema(items: { name: string; url: string }[]) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
'itemListElement': items.map((item, i) => ({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
'position': i + 1,
|
||||||
|
'name': item.name,
|
||||||
|
'item': item.url,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user