# Miksi et löytänyt tätä Googlesta (ja miten korjasin sen)

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

Olen pyörittänyt tätä blogia jo jonkin aikaa, kirjoittanut yksityiskohtaisia teknisiä artikkeleita ASP.NET Coresta, Entity Frameworkista, HTMX:stä ja kaikenlaisista .NET-hyvistä. Kuitenkin hakurankingini olivat... altavastaavia. Tutkittuani viimein miksi, huomasin sabotoineeni omaa SEO:tani joillakin aloittelijavirheillä.

[TOC]

# Ongelma: Jokainen sivu näytti Googlelle samanlaiselta

Kun Google ryömii sivustoasi, se katsoo useita keskeisiä signaaleja ymmärtääkseen, mistä kussakin sivussa on kyse. Lähetin Googlelle samoja signaaleja jokaiselle sivulle.

## Tupakointiase: Staattinen metakuvaus

Tässä on mitä minun `_Layout.cshtml` näytti:

```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.">
```

Jokainen. Single. Page. Sama kuvaus. Artikkelini [Taustapalvelut ASP.NET Coressa](/blog/background-services-in-aspnetcore-part1)Sama kuvaus kuin kotisivullani. [RAG-arkkitehtuuri](/blog/rag-architecture)Sama kuvaus.

Google näkee 200+-sivua, joilla on samanlaiset kuvaukset, ja katsoo, että "tällä sivustolla on päällekkäisiä sisältöongelmia" tai "tämä sivusto ei välitä hyödyllisestä tiedosta".

## Puuttuvat kanoniset URL-osoitteet

Minulla on monikielinen blogi, jossa on käännöksiä. Samaa sisältöä löytyy osoitteesta:

- `/blog/my-article` (englanti)
- `/blog/my-article/fr` (ranska)
- `/blog/my-article/de` (saksalainen)

Ilman kanonisia URL-osoitteita Google saattaa nähdä nämä sisällön kaksoiskappaleina, jotka laimentavat SEO-arvoa useiden URL-osoitteiden välillä sen sijaan, että ne vahvistettaisiin pääsivulle.

## Ei jäsenneltyjä tietoja

Googlen hakutuloksissa voi näkyä runsaasti näppäimiä - tekijän tietoja, julkaisupäiviä, artikkelien esikatseluja. Mutta vain, jos kerrot Googlelle näistä tiedoista käyttämällä [JSON-LD:n jäsennelty data](https://developers.google.com/search/docs/appearance/structured-data/article)En ollut.

## Staattinen sosiaalinen kuva

Jokainen sivu jakoi saman `og:image`. Vaikka tämä ei suoraan vaikuta Googlen rankingiin, se vaikuttaa klikkauksen läpi -hintoihin, kun artikkeleita jaetaan sosiaalisessa mediassa - mikä vaikuttaa epäsuorasti SEO:han sitoutumissignaalien kautta.

# Korjaukset

## 1. Dynamic Meta Descriptions

Ensimmäinen korjaus oli layout-tuen dynaamisten kuvausten tekeminen järkevällä varalla:

```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" />
    }
}
```

Nyt asettelussa lukee: `ViewBag.Description` Jos valittu, takaisin oletukseen. Kanoninen URL asetetaan automaattisesti nykyiselle sivulle URL.

## 2. Autogeneroidut kuvaukset blogikirjoituksiin

Jokainen blogikirjoitus tuottaa nyt kuvaustaan sisällön 155 ensimmäisestä merkistä. `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}";
}
```

Tärkeimmät seikat ovat seuraavat:

1. **Runkaa sanarajalla** - Viimeisen tilan ennen 155 merkkiä vältämme leikkaamasta sanoja kahtia
2. **Poista uudet linjat** - Metakuvausten pitäisi olla yksirivisiä
3. **Aseta `og:type` "hiukkanen"** - Kertoo sosiaalisille alustoille, että kyseessä on artikkeli, ei yleinen verkkosivu
4. **Läpäise artikkelin metatiedot** - Julkaistu päivämäärä ja kategoriat kulkevat layoutiin

## 3. JSON-LD Structured Data

Lisää JSON-LD-skriptiblokki blogisi postausnäkymään:

```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>
```

Huomaa, että `@@` Pakeneminen `@` symboli Razorin näköpiirissä - JSON-LD käyttää `@context` sekä `@type` joka muuten tulkittaisiin Razor syntaksiksi.

Tämä jäsennelty data kertoo Googlelle:

- **Millaista sisältöä tämä on?** (BlogPostia)
- **Kuka sen kirjoitti?** (Henkilö, jolla on URL ja lisätietoja)
- **Kun se julkaistiin ja muokattiin**
- **Mitä aiheita siinä käsitellään?** (avainsanat kategorioista)
- **Kuinka kauan se kestää?** (sanamäärä)
- **Millä kielellä se on?**

Google voi käyttää tätä näyttääkseen runsaasti hakutuloksia, mukaan lukien tekijän tiedot, julkaistakseen päivämääriä ja paljon muuta.

## 4. Ainutlaatuiset kuvaukset avainsivuille

Älä unohda staattisia sivujasi. Jokaisessa pitäisi olla ainutlaatuinen, merkityksellinen kuvaus:

```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.";
}
```

# Muut SEO-vaatimukset

## Sivukartta

Tarvitset sivustokartan. Tässä yksinkertainen ohjain, joka tuottaa yhden dynaamisesti:

```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");
    }
}
```

Rekisteröi reitti:

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

## robotit.txt

Kerro hakukoneille, missä sivustokarttasi on ja mitä ryömiä:

```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-syöte

Monet kehittäjät käyttävät RSS-lukijoita. RSS-syötteen käyttö auttaa myös löytämisessä:

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

# Entä kuvat?

Saatat miettiä ainutlaatuisten OG-kuvien tuottamista jokaiseen postaukseen. Teknisessä blogissa se ei todennäköisesti ole vaivan arvoista. Liikenne tulee:

- **Google-haku** - Kuvauksilla on enemmän merkitystä kuin kuvilla
- **RSS-syötteet** - ei kuvia
- **Hacker News / Reddit** - kynnet hädin tuskin näkyvissä
- **Suorat linkit** - URL-osoitteita jakavat kehittäjät

Johdonmukainen brändätty kuva sopii hyvin tunnistettavaksi. Custom-kuvasukupolvella on enemmän merkitystä visuaaliselle sisällölle, uutissivustoille tai markkinointisivuille, jotka kilpailevat Facebookin ja Twitterin klikkauksista.

Jos haluat luoda ne, voit käyttää [ImageSharp](https://docs.sixlabors.com/articles/imagesharp/) (jota käytän jo kuvankäsittelyyn) tekstin päällystämiseen mallilla:

```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";
}
```

Mutta teknisessä blogissa saat mitä sinulla on.

# SEO:n testaaminen

## Google Richin tulostesti

Käyttö [Googlen runsaat tulokset -testi](https://search.google.com/test/rich-results) Vahvistaaksesi jäsennellyt tietosi. Liitä URL ja se kertoo, onko JSON-LD pätevä ja mitä hyviä tuloksia olet oikeutettu saamaan.

## Näytä sivulähde

Yksinkertaisin testi - katso sivun lähde ja tarkista:

- Onko `<meta name="description">` ainutlaatuista tälle sivulle?
- Onko olemassa `<link rel="canonical">`?
- Onko olemassa `<script type="application/ld+json">` Block?

## Google-hakukonsoli

Käyttöönoton jälkeen lähetä sivustokarttasi [Google-hakukonsoli](https://search.google.com/search-console). Voit myös käyttää URL Inspection -työkalua nähdäksesi tarkasti, miten Google näkee sivusi ja pyytää uudelleen indeksointia.

# Yhteenveto

Korjaukset olivat yksinkertaisia:

Ongelma korjaantuu
|---------|-----|
Staattinen metakuvaus Dynamic `ViewBag.Description` Autogeneration from content
Ei kanonisia URL-osoitteita Lisätty `<link rel="canonical">` asetteluun
Ei jäsenneltyjä tietoja JSON-LD `BlogPosting` Skema jokaisessa virassa
Puuttuvat artikkelin metatiedot `article:published_time`, `article:author`, `article:tag` Metatunnisteet
Staattinen sivun kuvaukset Ainutlaatuinen kuvaukset Kotiin, Blogi, Yhteystiedot, Hakusivut

Avainnäkemys: **Google ei osaa lukea ajatuksiasi**. Jos jokaisella sivulla on sama kuvaus, Googlella ei ole keinoa tietää, mikä tekee jokaisesta sivusta ainutlaatuisen ja arvokkaan. Kerro Googlelle, mistä on kyse, niin arvosi on parempi.

Anna Googlelle muutama viikko aikaa avata sivustosi uudelleen, ja sinun pitäisi nähdä parannuksia. SEO on pitkä peli, mutta nämä perusasiat ovat perusta, jolle kaikki muu rakentuu.

# Lisää luettavaa

- [Googlen SEO-käynnistysopas](https://developers.google.com/search/docs/fundamentals/seo-starter-guide)
- [Schema.org BlogPosting](https://schema.org/BlogPosting)
- [Avoin kaavioprotokolla](https://ogp.me/)
- [Google Richin tulostesti](https://search.google.com/test/rich-results)
- [Google-hakukonsoli](https://search.google.com/search-console)