feat: add og tags
Some checks failed
release-tag / build-and-deploy (push) Has been cancelled

This commit is contained in:
Zotov Ivan Yuryevich
2026-08-15 17:58:58 +03:00
parent 4b368d66e9
commit ed5677bcfb
13 changed files with 114 additions and 10 deletions

2
src/env.d.ts vendored
View File

@@ -4,6 +4,8 @@ interface ImportMetaEnv {
readonly PUBLIC_API_URL: string
readonly PUBLIC_GRAPHQL_URL: string
readonly PUBLIC_UPLOADS_BASE_URL: string
/** Public origin of the site, e.g. https://softclick.ru — used for canonical/og:url. */
readonly PUBLIC_SITE_URL?: string
readonly BITRIX24_WEBHOOK_URL?: string
}

View File

@@ -4,6 +4,7 @@ export const privacyPolicyDocument = gql`
query PrivacyPolicy {
siteConfig {
title
description
privacyPolicy
}
}

View File

@@ -4,6 +4,7 @@ export const termsOfUseDocument = gql`
query TermsOfUse {
siteConfig {
title
description
termsOfUse
}
}

View File

@@ -1632,12 +1632,12 @@ export type SiteConfigQuery = { __typename?: 'Query', siteConfig?: { __typename?
export type PrivacyPolicyQueryVariables = Exact<{ [key: string]: never; }>;
export type PrivacyPolicyQuery = { __typename?: 'Query', siteConfig?: { __typename?: 'SiteConfig', title: string, privacyPolicy?: string | null } | null };
export type PrivacyPolicyQuery = { __typename?: 'Query', siteConfig?: { __typename?: 'SiteConfig', title: string, description?: string | null, privacyPolicy?: string | null } | null };
export type TermsOfUseQueryVariables = Exact<{ [key: string]: never; }>;
export type TermsOfUseQuery = { __typename?: 'Query', siteConfig?: { __typename?: 'SiteConfig', title: string, termsOfUse?: string | null } | null };
export type TermsOfUseQuery = { __typename?: 'Query', siteConfig?: { __typename?: 'SiteConfig', title: string, description?: string | null, termsOfUse?: string | null } | null };
export class TypedDocumentString<TResult, TVariables>
extends String
@@ -1806,6 +1806,7 @@ export const PrivacyPolicyDocument = new TypedDocumentString(`
query PrivacyPolicy {
siteConfig {
title
description
privacyPolicy
}
}
@@ -1814,6 +1815,7 @@ export const TermsOfUseDocument = new TypedDocumentString(`
query TermsOfUse {
siteConfig {
title
description
termsOfUse
}
}

View File

@@ -4,14 +4,20 @@ import '../styles/globals.css'
interface Props {
siteTitle: string
siteDescription?: string
pageTitle: string
contentHtml: string
}
const { siteTitle, pageTitle, contentHtml } = Astro.props
const { siteTitle, siteDescription = '', pageTitle, contentHtml } = Astro.props
---
<Layout title={`${pageTitle} | ${siteTitle}`}>
<Layout
title={`${pageTitle} | ${siteTitle}`}
description={siteDescription}
siteName={siteTitle}
type="article"
>
<div class="min-h-screen bg-white flex flex-col">
<header class="border-b border-gray-200">
<div class="max-w-3xl mx-auto px-4 h-16 flex items-center justify-between w-full">

View File

@@ -2,20 +2,82 @@
import ClientRouter from 'astro/components/ClientRouter.astro'
interface Props {
/** Full <title> / og:title. Comes from Strapi (siteConfig.title, category.title). */
title?: string
/** meta description / og:description. Comes from Strapi. */
description?: string
/** og:site_name — the brand, without the per-page part. */
siteName?: string
/** og:type: 'website' for the home page, 'article' for content pages. */
type?: 'website' | 'article'
/** Absolute or site-root-relative image for og:image / twitter:image. */
image?: string
/** Pixel size of `image` — crawlers use it to lay the preview out before download. */
imageWidth?: number
imageHeight?: number
/** Override the canonical path (defaults to the current URL without query). */
canonicalPath?: string
/** Keep the page out of search indexes (error pages, gated pages). */
noindex?: boolean
}
const { title = '', description = '' } = Astro.props
const {
title = '',
description = '',
siteName = '',
type = 'website',
image = '/logo.jpg',
imageWidth = 1024, // matches public/logo.jpg
imageHeight = 1024,
canonicalPath,
noindex = false,
} = Astro.props
// Absolute URLs are required by Open Graph — relative ones are ignored by most
// crawlers and social scrapers. PUBLIC_SITE_URL pins the public origin; behind a
// reverse proxy Astro.url alone can resolve to the internal container host.
// Prefer process.env so the domain can be set at deploy/runtime (Docker), with
// import.meta.env as the `astro dev` fallback (see src/middleware.ts).
const siteOrigin =
process.env.PUBLIC_SITE_URL || import.meta.env.PUBLIC_SITE_URL || Astro.url.origin
const canonicalURL = new URL(canonicalPath ?? Astro.url.pathname, siteOrigin)
const imageURL = new URL(image, siteOrigin)
---
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL.href} />
{
noindex ? (
<meta name="robots" content="noindex, nofollow" />
) : (
<meta name="robots" content="index, follow, max-image-preview:large" />
)
}
<!-- Open Graph (VK, Telegram, WhatsApp, LinkedIn, Facebook) -->
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalURL.href} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{siteName && <meta property="og:site_name" content={siteName} />}
<meta property="og:locale" content="ru_RU" />
<meta property="og:image" content={imageURL.href} />
<meta property="og:image:width" content={String(imageWidth)} />
<meta property="og:image:height" content={String(imageHeight)} />
<meta property="og:image:alt" content={title} />
<!-- Twitter/X card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageURL.href} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<ClientRouter />
<script is:inline type="text/javascript">
// prettier-ignore

View File

@@ -15,7 +15,12 @@ try {
}
---
<Layout title={siteTitle} description={siteDescription}>
<Layout
title={siteTitle ? `Страница не найдена | ${siteTitle}` : 'Страница не найдена'}
description={siteDescription}
siteName={siteTitle}
noindex
>
<div class="min-h-screen grid content-center justify-items-center">
<div>404</div>
<div>Страница не найдена</div>

View File

@@ -15,7 +15,12 @@ try {
}
---
<Layout title={siteTitle} description={siteDescription}>
<Layout
title={siteTitle ? `Ошибка сервера | ${siteTitle}` : 'Ошибка сервера'}
description={siteDescription}
siteName={siteTitle}
noindex
>
<div class="min-h-screen grid content-center justify-items-center">
<div>500</div>
<div>Мы скоро всё починим</div>

View File

@@ -67,6 +67,9 @@ const vendors = (category.vendors?.filter(v => v != null) ?? []).map(vendor => (
<Layout
title={title ? `${title} | ${siteTitle}` : siteTitle}
description={description ?? siteDescription}
siteName={siteTitle}
type="article"
canonicalPath={`/${slug}`}
>
<div class="min-h-screen bg-white w-full overflow-x-hidden">
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} sectionAnchors={sectionAnchors} />

View File

@@ -6,6 +6,11 @@
// It is intentionally self-contained — no shared Layout and no Strapi data — so
// it renders even while the rest of the site is gated.
Astro.response.headers.set('X-Robots-Tag', 'noindex, nofollow')
// Same origin resolution as src/layouts/Layout.astro — og:image must be absolute.
const siteOrigin =
process.env.PUBLIC_SITE_URL || import.meta.env.PUBLIC_SITE_URL || Astro.url.origin
const ogImage = new URL('/logo.jpg', siteOrigin).href
---
<!doctype html>
@@ -15,6 +20,16 @@ Astro.response.headers.set('X-Robots-Tag', 'noindex, nofollow')
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>СОФТКЛИК — сайт в разработке</title>
<meta name="description" content="Сайт в разработке. Следите за обновлениями." />
<!-- Kept for messenger link previews (the preview link gets shared in chats);
indexing is still blocked by the robots meta above. -->
<meta property="og:type" content="website" />
<meta property="og:title" content="СОФТКЛИК — сайт в разработке" />
<meta property="og:description" content="Сайт в разработке. Следите за обновлениями." />
<meta property="og:site_name" content="СОФТКЛИК" />
<meta property="og:locale" content="ru_RU" />
<meta property="og:image" content={ogImage} />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<div class="screen">

View File

@@ -40,7 +40,7 @@ const serviceCategories = menuItems.filter(c => c.group === Enum_Category_Group.
const sectionAnchors = getHomeSectionAnchors(homePage)
---
<Layout title={siteTitle} description={siteDescription}>
<Layout title={siteTitle} description={siteDescription} siteName={siteTitle} type="website">
<div class="min-h-screen bg-white w-full overflow-x-hidden">
<Header client:load siteTitle={siteTitle} contacts={contacts} solutions={solutionCategories} services={serviceCategories} sectionAnchors={sectionAnchors} />
<HomePage client:load homePage={homePage} contacts={contacts} />

View File

@@ -15,6 +15,7 @@ const contentHtml = markdown ? await renderMarkdown(markdown) : ''
<DocumentLayout
siteTitle={siteConfig?.title ?? ''}
siteDescription={siteConfig?.description ?? ''}
pageTitle="Политика конфиденциальности"
contentHtml={contentHtml}
/>

View File

@@ -15,6 +15,7 @@ const contentHtml = markdown ? await renderMarkdown(markdown) : ''
<DocumentLayout
siteTitle={siteConfig?.title ?? ''}
siteDescription={siteConfig?.description ?? ''}
pageTitle="Условия использования"
contentHtml={contentHtml}
/>