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é.
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.
Esto es lo que mi _Layout.cshtml se veía como:
<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 CoreLa misma descripción que mi página de inicio. RAG ArchitectureLa 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.
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.
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-LDNo lo estaba.
Cada página compartida es la misma og:imageAunque 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.
La primera solución fue hacer las descripciones dinámicas de soporte de diseño con una copia de seguridad sensible:
@{
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.
Cada entrada de blog genera ahora su descripción a partir de los primeros 155 caracteres de contenido. Post.cshtml:
@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í:
og:type al "artículo" - Le dice a las plataformas sociales que este es un artículo, no una página web genéricaEste es el grande para los fragmentos ricos. Agregue un bloque de script JSON-LD a su vista de post blog:
<!-- 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:
Google puede utilizar esto para mostrar fragmentos ricos en resultados de búsqueda, incluyendo información del autor, publicar fechas, y más.
No olvide sus páginas estáticas. Cada una debe tener una descripción única y relevante:
<!-- 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.";
}
Necesita un mapa de sitio. Aquí hay un controlador simple que genera uno dinámicamente:
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:
app.MapControllerRoute(
name: "sitemap",
pattern: "sitemap.xml",
defaults: new { controller = "SiteMap", action = "Index" });
Dígale a los motores de búsqueda dónde está su mapa de sitio y qué arrastrar:
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);
});
Muchos desarrolladores utilizan lectores RSS. Tener un feed RSS también ayuda a descubrir:
<link rel="alternate" type="application/atom+xml"
title="RSS Feed for mostlylucid.net"
href="https://mostlylucid.net/rss" />
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:
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 (que ya uso para el procesamiento de imágenes) para superponer texto en una plantilla:
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.
Uso Prueba de resultados ricos de Google 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.
La prueba más simple - ver la fuente de la página y comprobar:
<meta name="description"> único para esta página?<link rel="canonical">?<script type="application/ld+json"> ¿Bloquear?Después de implementar, envíe su mapa de sitio a Consola de búsqueda de Google. 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.
Las correcciones fueron sencillas:
|---------|-----|
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.