# Чому ви не знайшли це в Google (і як я його виписував)

<!--category-- ASP.NET Core, SEO -->
<datetime class="hidden">2025-11-26T14:00</datetime>

Я веду цей блог вже деякий час, пишу детальні технічні статті про ядро ASP.NET, City Framework, HTMX і всі види NET добропорядності.

[TOC]

# Проблема: кожна сторінка виглядала однаковою до Google

Коли Google повзає вашим сайтом, він бачить декілька ключових сигналів, щоб зрозуміти, про що йдеться на кожній сторінці.

## Зброя для куріння: Статичні мета описи

Ось що моє `_Layout.cshtml` виглядало так:

```html
<meta name="description" content="Scott Galloway is a lead developer and software engineer with a passion for building web applications.">
<meta property="og:description" content="Scott Galloway is a lead developer and software engineer with a passion for building web applications.">
```

Все, одинока сторінка, той самий опис, моя стаття про [Служби тла у ядрі ASP. NET](/blog/background-services-in-aspnetcore-part1)♪ [Архітектура RAG](/blog/rag-architecture), той самий опис.

Google бачить сторінки 200+ з ідентичними описами і думає, що "цей сайт має дублікати проблем з вмістом" або "цьому сайту байдуже до надання корисної інформації." У будь-якому разі, рейтинги страждають.

## Відсутні Canonical URL

У мене є багатомовний блог з перекладами. Той самий вміст існує у:

- `/blog/my-article` (англійською)
- `/blog/my-article/fr` (французька)
- `/blog/my-article/de` (німецькою)

Без канонічних адрес URL, Google може вважати ці адреси дублікатами вмісту, зменшуючи значення SEO у декількох URLх, замість того, щоб об'єднувати їх на головній сторінці.

## Немає структурованих даних

Результати пошуку Google можуть показувати багато інформації про авторів, опублікувати дати, попередній перегляд статей. [Базові дані JSON-LD](https://developers.google.com/search/docs/appearance/structured-data/article)Я не была.

## Статичні соціальні зображення

Кожна зі сторінок була однаковою `og:image`Хоча це не безпосередньо впливає на рейтинг Google, це впливає на клік-ударів, коли статті поділяються на соціальні медіа, що непрямо впливає на пошукову оптимізацію через сигнали залучення.

# Фіксації

## 1. Динамічні мета описи

Першою розв' язкою було створення підтримки динамічного опису компонування з розумним поверненням:

```html
@{
    var currentUrl = $"https://{Context.Request.Host}{Context.Request.Path}";
    var defaultDescription = "Scott Galloway is a lead developer and software engineer with a passion for building web applications.";
    var pageDescription = ViewBag.Description as string ?? defaultDescription;
}

<!-- Canonical URL -->
<link rel="canonical" href="@currentUrl" />

<!-- Facebook Meta Tags -->
<meta property="og:url" content="@currentUrl">
<meta property="og:type" content="@(ViewBag.OgType ?? "website")">
<meta property="og:title" content="@ViewBag.Title">
<meta property="og:description" content="@pageDescription">
<meta property="og:site_name" content="mostlylucid" />

<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="@ViewBag.Title">
<meta name="twitter:description" content="@pageDescription">

<!-- Meta Description -->
<meta name="description" content="@pageDescription" />

<!-- Article metadata for blog posts -->
@if (ViewBag.PublishedDate != null)
{
    <meta property="article:published_time" content="@(((DateTime)ViewBag.PublishedDate).ToString("yyyy-MM-ddTHH:mm:ssZ"))" />
    <meta property="article:author" content="Scott Galloway" />
}
@if (ViewBag.Categories != null)
{
    foreach (var category in ViewBag.Categories)
    {
        <meta property="article:tag" content="@category" />
    }
}
```

Тепер компонування читається `ViewBag.Description` якщо встановлено, повернення до типової адреси. Еквівалентну адресу URL буде автоматично встановлено до поточної адреси URL сторінки.

## 2. Автоматичні описи дописів блогу

Кожен допис блогу створює свій опис з перших 155 символів вмісту. `Post.cshtml`:

```html
@using Mostlylucid.Shared.Helpers
@model Mostlylucid.Models.Blog.BlogPostViewModel

@{
    Layout = "_Layout";
    ViewBag.Title = $"{Model.Title} ({Model.Language.ConvertCodeToLanguage()})";

    // Generate description from plain text content (first 155 chars, truncate at word boundary)
    var plainText = Model.PlainTextContent ?? "";
    var description = plainText.Length > 155
        ? plainText.Substring(0, plainText.LastIndexOf(' ', 155)) + "..."
        : plainText;
    description = description.Replace("\n", " ").Replace("\r", "").Trim();
    ViewBag.Description = description;

    // Set article metadata
    ViewBag.OgType = "article";
    ViewBag.PublishedDate = Model.PublishedDate;
    ViewBag.Categories = Model.Categories;

    // Build canonical URL (without language suffix for English)
    var canonicalUrl = Model.Language == "en"
        ? $"https://{Context.Request.Host}/blog/{Model.Slug}"
        : $"https://{Context.Request.Host}/blog/{Model.Slug}/{Model.Language}";
}
```

Ключові точки тут:

1. **Обрізати на межі слова** - Ми знаходимо останній проміжок до 155 символів, щоб уникнути скорочення слів навпіл.
2. **Скидати рядки** - Мета описи повинні бути однорядковими
3. **Встановити `og:type` до "шарової"** - Повідомляє соціальні платформи Це стаття, а не загальна веб-сторінка
4. **Передати метадані статті** - Оприлюднена дата і категорії передаються до компонування

## 3. Дані структури JSON- LD

Це велика частина для багатих фрагментів. Додайте до вашого поля блогу блок скрипту JSON- LD:

```html
<!-- JSON-LD Structured Data for Blog Post -->
<script type="application/ld+json">
{
    "@@context": "https://schema.org",
    "@@type": "BlogPosting",
    "headline": "@Model.Title",
    "description": "@description",
    "datePublished": "@Model.PublishedDate.ToString("yyyy-MM-ddTHH:mm:ssZ")",
    @if (Model.UpdatedDate.HasValue)
    {
        @:"dateModified": "@Model.UpdatedDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ")",
    }
    "author": {
        "@@type": "Person",
        "name": "Scott Galloway",
        "url": "https://mostlylucid.net/blog/aboutme"
    },
    "publisher": {
        "@@type": "Organization",
        "name": "mostlylucid",
        "logo": {
            "@@type": "ImageObject",
            "url": "https://mostlylucid.net/img/logo.svg"
        }
    },
    "mainEntityOfPage": {
        "@@type": "WebPage",
        "@@id": "@canonicalUrl"
    },
    "wordCount": @Model.WordCount,
    "inLanguage": "@Model.Language",
    "keywords": "@string.Join(", ", Model.Categories)",
    "image": "https://mostlylucid.net/img/social2.jpg"
}
</script>
```

Зауважте `@@` тікайте, щоб уникнути `@` символ у переглядах Razor - використання JSON- LD `@context` і `@type` який в іншому випадку інтерпретується як синтаксис Razor.

Ці структуровані дані говорять Google:

- **Що це за тип вмісту** (Блог-Позиція)
- **Хто це написав?** (Об' єднується з URL, щоб дізнатися більше)
- **Після опублікування і зміни**
- **Що пов'язано з темою** (ключі з категорій)
- **Скільки часу минуло** (Слово число)
- **Якою мовою вона є в**

Google може використовувати цю можливість, щоб показати багато фрагментів у результатах пошуку, зокрема, дані авторів, дати оприлюднення тощо.

## 4. Унікальні описи сторінок ключів

Не забудьте ваші статичні сторінки. Кожен з них повинен мати унікальний, доречний опис:

```html
<!-- Home page -->
@{
    ViewBag.Title = "mostlylucid- Scott Galloway's Developer Blog";
    ViewBag.Description = "Technical blog covering ASP.NET Core, C#, Entity Framework, HTMX, Docker, and modern web development. Practical tutorials, NuGet packages, and open source projects.";
}

<!-- Blog index -->
@{
    ViewBag.Title = "Blog Posts";
    ViewBag.Description = "Technical articles on ASP.NET Core, C#, Entity Framework, Docker, and modern web development. Practical tutorials and real-world examples from a lead developer.";
}

<!-- Contact page -->
@{
    ViewBag.Title = "Contact Scott Galloway";
    ViewBag.Description = "Get in touch with Scott Galloway. Questions about ASP.NET Core, C#, web development, or collaboration opportunities? Send me a message.";
}

<!-- Search page -->
@{
    ViewBag.Title = "Search Results";
    ViewBag.Description = "Search through technical articles on ASP.NET Core, C#, Entity Framework, and web development. Find tutorials, guides, and solutions.";
}
```

# Інші потреби

## Карта сайта

Вам потрібна карта сайтів. Ось простий контролер, який створює її динамічно:

```csharp
public class SiteMapController(
    IBlogViewService blogViewService,
    IHttpContextAccessor httpContextAccessor) : Controller
{
    [HttpGet]
    [ResponseCache(Duration = 43200)] // Cache for 12 hours
    public async Task<IActionResult> Index()
    {
        var pages = await blogViewService.GetPosts();
        var siteUrl = $"https://{httpContextAccessor.HttpContext?.Request.Host}";

        XNamespace sitemap = "http://www.sitemaps.org/schemas/sitemap/0.9";

        var feed = new XDocument(
            new XDeclaration("1.0", "utf-8", null),
            new XElement(sitemap + "urlset",
                from page in pages
                select new XElement(sitemap + "url",
                    new XElement(sitemap + "loc", $"{siteUrl}/blog/{page.Slug}"),
                    new XElement(sitemap + "lastmod", page.PublishedDate.ToString("yyyy-MM-dd")),
                    new XElement(sitemap + "changefreq", "weekly"),
                    new XElement(sitemap + "priority", "0.8")
                )
            )
        );

        return Content(feed.ToString(), "text/xml");
    }
}
```

Зареєструвати маршрут:

```csharp
app.MapControllerRoute(
    name: "sitemap",
    pattern: "sitemap.xml",
    defaults: new { controller = "SiteMap", action = "Index" });
```

## robots. txt

Повідомити пошуковим рушіям, де знаходиться карта вашого сайту, і про те, що слід пересуватися:

```csharp
app.MapGet("/robots.txt", async context =>
{
    var siteUrl = $"https://{context.Request.Host}";
    var robotsTxt = $"""
        User-agent: *
        Allow: /

        Sitemap: {siteUrl}/sitemap.xml
        """;

    context.Response.ContentType = "text/plain";
    await context.Response.WriteAsync(robotsTxt);
});
```

## Подача RSS

Багато розробників користуються читачами RSS. Використання подачі RSS також допомагає з можливістю відкриття:

```html
<link rel="alternate" type="application/atom+xml"
      title="RSS Feed for mostlylucid.net"
      href="https://mostlylucid.net/rss" />
```

# А що сказати про зображення?

Вам, можливо, цікаво створити унікальні зображення OG для кожного допису. Для технічного блогу це, ймовірно, не варте зусиль. Ваш трафік походить від:

- **Пошук у Google** - описи мають більше значення, ніж зображення
- **Подачі RSS** - немає зображень
- **Hacker News / Reddit** - Мініатюри ледь помітні
- **Прямі посилання** - розробники з спільними адресами URL

Постійне брендове зображення підходить для розпізнавання. Створення нетипових зображень має більше значення для візуального вмісту, сайтів новин або маркетингових сторінок, які змагаються за натискання на Facebook і Twitter.

Якщо ви справді бажаєте їх створити, ви можете скористатися [Розширення зображеньComment](https://docs.sixlabors.com/articles/imagesharp/) (яку я вже використовував для обробки зображень) для накладання тексту на шаблон:

```csharp
public async Task<string> GenerateOgImage(string title, string slug)
{
    using var image = await Image.LoadAsync("wwwroot/img/og-template.png");

    var font = SystemFonts.CreateFont("Arial", 48, FontStyle.Bold);

    image.Mutate(x => x.DrawText(
        new RichTextOptions(font)
        {
            Origin = new PointF(50, 200),
            WrappingLength = 1100
        },
        title,
        Color.White));

    var outputPath = $"wwwroot/og/{slug}.png";
    await image.SaveAsPngAsync(outputPath);
    return $"/og/{slug}.png";
}
```

Але для технічного блогу?

# Перевірка пошукової оптимізації

## Тест багатих результатів Google

Користування [Тест багатих результатів Google](https://search.google.com/test/rich-results) для перевірки структури ваших даних. Вставте адресу URL і вона повідомить вам, чи ваш JSON- LD є коректним і для чого потрібні багаті результати.

## Переглянути джерело сторінки

Найпростіша перевірка - перегляд джерел сторінок і перевірка:

- Чи є `<meta name="description">` унікально для цієї сторінки?
- Є є `<link rel="canonical">`?
- Є є `<script type="application/ld+json">` блок?

## Консоль пошуку Google

Після запуску надішліть вашу карту сайта до [Консоль пошуку Google](https://search.google.com/search-console)Ви також можете скористатися інструментом Inspection, щоб побачити, як Google бачить ваші сторінки та запити на повторне дослідження.

# Зведення

Виправлення були простими:

♪
|---------|-----|
 `ViewBag.Description` з автопоколінням з вмісту}
 Без адрес URL * * * _BAR_\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ >\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ > `<link rel="canonical">` до circle сягменту
♪ Без об'єму даних} JSON- LD `BlogPosting` schema on every post}
*  Статтєва стаття} `article:published_time`, `article:author`, `article:tag` Метастази
ТАЙМЕТИЧНА ПАСТКА: Home, Blog, Conact, Search pages ♪

Головне розуміння: **Гугл не може читати ваші думки**... Якщо кожна сторінка має однаковий опис, Гугл не може знати, що робить кожну сторінку унікальною і цінною.

Дайте Google декілька тижнів для перерозподілу вашого сайту, і ви побачите покращення. SEO - це довга гра, але ці фундаментальні принципи є основою всього іншого.

# Подальше читання

- [Інструкція для започаткування SEO для Google](https://developers.google.com/search/docs/fundamentals/seo-starter-guide)
- [Schema.org BlogPosting](https://schema.org/BlogPosting)
- [Відкрити протокол графу](https://ogp.me/)
- [Тест багатих результатів Google](https://search.google.com/test/rich-results)
- [Консоль пошуку Google](https://search.google.com/search-console)