# Por qué no encontraste esto en Google (y cómo lo arreglé)

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

He estado ejecutando este blog por un tiempo ahora, escribiendo artículos técnicos detallados en ASP.NET Core, Entity Framework, HTMX, y todo tipo de bondad .NET. Sin embargo, mis rankings de búsqueda eran... abrumadores. Después de finalmente investigar por qué, descubrí que había estado saboteando mi propio SEO con algunos errores de novato. He aquí lo que estaba mal y cómo lo arreglé.

[TOC]

# El problema: Cada página parecía idéntica a Google

Cuando Google rastrea su sitio, mira varias señales clave para entender de qué se trata cada página. Yo estaba enviando Google las mismas señales para cada página.

## La pistola para fumar: Descripciones de Meta estáticas

Esto es lo que mi `_Layout.cshtml` se veía como:

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

Todos. Individual. Página. La misma descripción. Mi artículo sobre [Servicios de antecedentes en ASP.NET Core](/blog/background-services-in-aspnetcore-part1)La misma descripción que mi página de inicio. [RAG Architecture](/blog/rag-architecture)La misma descripción.

Google ve más de 200 páginas con descripciones idénticas y piensa "este sitio tiene problemas de contenido duplicados" o "este sitio no le importa proporcionar información útil." De cualquier manera, los rankings sufren.

## Faltan URL canónicas

Tengo un blog multilingüe con traducciones. El mismo contenido existe en:

- `/blog/my-article` (Inglés)
- `/blog/my-article/fr` (Francés)
- `/blog/my-article/de` (Alemán)

Sin URLs canónicas, Google podría ver estos como contenido duplicado, diluyendo el valor SEO a través de múltiples URLs en lugar de consolidarlo en la página principal.

## Sin datos estructurados

Los resultados de búsqueda de Google pueden mostrar fragmentos ricos - información del autor, publicar fechas, previsualizaciones del artículo. Pero sólo si usted le dice a Google acerca de estos datos utilizando [Datos estructurados de JSON-LD](https://developers.google.com/search/docs/appearance/structured-data/article)No lo estaba.

## Imágenes sociales estáticas

Cada página compartida es la misma `og:image`Aunque esto no afecta directamente a los rankings de Google, afecta a las tasas de clics cuando los artículos se comparten en las redes sociales, lo que afecta indirectamente al SEO a través de señales de compromiso.

# Los Fixes

## 1. Descripciones de Meta Dinámicas

La primera solución fue hacer las descripciones dinámicas de soporte de diseño con una copia de seguridad sensible:

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

Ahora el diseño lee `ViewBag.Description` si se establece, cayendo de nuevo a un predeterminado. La URL canónica se establece automáticamente a la URL de la página actual.

## 2. Descripciones autogeneradas para entradas de blog

Cada entrada de blog genera ahora su descripción a partir de los primeros 155 caracteres de contenido. `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}";
}
```

Los puntos clave aquí:

1. **Troncar en el límite de la palabra** - Encontramos el último espacio antes de 155 caracteres para evitar cortar palabras a la mitad
2. **Strip newlines** - Meta descripciones deben ser de una sola línea
3. **Establecer `og:type` al "artículo"** - Le dice a las plataformas sociales que este es un artículo, no una página web genérica
4. **Pasar metadatos de artículos** - La fecha de publicación y las categorías fluyen a través de la disposición

## 3. Datos estructurados JSON-LD

Este es el grande para los fragmentos ricos. Agregue un bloque de script JSON-LD a su vista de post blog:

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

Nótese que: `@@` escapando para el `@` símbolo en vistas Razor - JSON-LD utiliza `@context` y `@type` que de otro modo se interpretaría como sintaxis de Razor.

Estos datos estructurados le dice a Google:

- **¿Qué tipo de contenido es este?** (BlogPosting)
- **¿Quién lo escribió?** (Persona con URL para obtener más información)
- **Cuando fue publicado y modificado**
- **¿Qué temas cubre?** (palabras clave de las categorías)
- **¿Cuánto tiempo es?** (Contador de palabras)
- **¿En qué idioma está?**

Google puede utilizar esto para mostrar fragmentos ricos en resultados de búsqueda, incluyendo información del autor, publicar fechas, y más.

## 4. Descripciones únicas para las páginas clave

No olvide sus páginas estáticas. Cada una debe tener una descripción única y relevante:

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

# Otros esenciales de SEO

## Mapa del sitio

Necesita un mapa de sitio. Aquí hay un controlador simple que genera uno dinámicamente:

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

Registrar la ruta:

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

## robots.txt

Dígale a los motores de búsqueda dónde está su mapa de sitio y qué arrastrar:

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

## Fuente RSS

Muchos desarrolladores utilizan lectores RSS. Tener un feed RSS también ayuda a descubrir:

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

# ¿ Qué hay de las imágenes?

Usted podría estar preguntándose acerca de generar imágenes OG únicas para cada post. Para un blog técnico, probablemente no vale la pena el esfuerzo. Su tráfico viene de:

- **Búsqueda en Google** - las descripciones importan más que las imágenes
- **Fuentes RSS** - sin imágenes
- **Hacker News / Reddit** - miniaturas apenas visibles
- **Vínculos directos** - desarrolladores compartiendo URLs

Una imagen de marca consistente está bien para el reconocimiento. Generar imágenes personalizadas importa más para el contenido visual, sitios de noticias o páginas de marketing que compiten por clics en Facebook y Twitter.

Si quieres generarlos, puedes utilizarlos. [ImageSharp](https://docs.sixlabors.com/articles/imagesharp/) (que ya uso para el procesamiento de imágenes) para superponer texto en una plantilla:

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

Pero para un blog técnico, envía lo que tienes.

# Probando tu SEO

## Prueba de resultados de Google Rich

Uso [Prueba de resultados ricos de Google](https://search.google.com/test/rich-results) para validar tus datos estructurados. Pega una URL y te dirá si tu JSON-LD es válido y a qué ricos resultados eres elegible.

## Ver fuente de página

La prueba más simple - ver la fuente de la página y comprobar:

- Es la `<meta name="description">` único para esta página?
- ¿Hay un `<link rel="canonical">`?
- ¿Hay un `<script type="application/ld+json">` ¿Bloquear?

## Consola de búsqueda de Google

Después de implementar, envíe su mapa de sitio a [Consola de búsqueda de Google](https://search.google.com/search-console). También puede utilizar la herramienta de inspección de URL para ver exactamente cómo Google ve sus páginas y solicitar re-indexación.

# Resumen

Las correcciones fueron sencillas:

# Problema # # Solución #
|---------|-----|
Metas descripciones estáticas Dinámicas `ViewBag.Description` con auto-generación a partir de contenido
No hay URL canónicos  Añadido `<link rel="canonical">` a la disposición
No hay datos estructurados  JSON-LD `BlogPosting` esquema en cada post
Faltan metadatos del artículo `article:published_time`, `article:author`, `article:tag` metaetiquetas
descripciones de páginas estáticas  descripciones únicas para Inicio, Blog, Contacto, Páginas de búsqueda

La visión clave: **Google no puede leer tu mente**. Si cada página tiene la misma descripción , Google no tiene manera de saber lo que hace que cada página única y valiosa . Dile a Google de lo que se trata cada página , y se clasificará mejor .

Dale unas semanas para que Google vuelva a crear tu sitio, y deberías ver mejoras. SEO es un juego largo, pero estos fundamentos son la base sobre la que se basa todo lo demás.

# Lectura adicional

- [Guía de inicio de SEO de Google](https://developers.google.com/search/docs/fundamentals/seo-starter-guide)
- [Schema.org BlogPosting](https://schema.org/BlogPosting)
- [Abrir el protocolo de gráfico](https://ogp.me/)
- [Prueba de resultados de Google Rich](https://search.google.com/test/rich-results)
- [Consola de búsqueda de Google](https://search.google.com/search-console)