# 为什么你没有在谷歌上找到这个(以及我如何固定它)

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

我经营这个博客已经有一段时间了, 写了有关ASP.NET核心、实体框架、HTMX和各种.NET的技术性文章。然而我的搜索排名却... 很低。在调查了为什么之后, 我发现我用一些新手的错误来破坏我自己的SEO。这里是错的,我是如何修补的。

[TOC]

# 问题:每页都与谷歌相同

当谷歌爬行你的网站时,它会看到几个关键信号来理解每一页的内容。 我向谷歌发送每页相同的信号。

## 《吸烟枪:静态元描述》

这是我的 `_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页以上的页面, 都有相同的描述, 认为「这个网站有重复的内容问题」或「这个网站不关心提供有用的信息」。

## 缺失的卡尼基 URL

我有一个多语种博客,

- `/blog/my-article` (英文)
- `/blog/my-article/fr` (法文)
- `/blog/my-article/de` (德文)

谷歌可能认为这些内容是重复内容, 将SEO值通过多个URL去稀释, 而不是将其合并在主页上。

## 无结构数据

Google的搜索结果可以显示丰富的片段 - 作者信息、 发布日期、 文章预览。 但是, 只有当您告诉 Google 使用此数据时 。 [JSON-LLD结构数据](https://developers.google.com/search/docs/appearance/structured-data/article)我没有

## 静静社会形象

每页同页 `og:image`虽然这并不直接影响Google的排名,但如果在社交媒体上分享文章,则会影响点击率,这通过参与信号间接影响SEO。

# 定法

## 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语法。

这个结构化数据告诉谷歌:

- **这是哪类内容** (布洛克插座)
- **是谁写的** (拥有 URL 以学习更多)
- **出版和修改时**
- **A. 它所涵盖的专题** (类别的关键词)
- **持续了多久** (数字)
- **是什么语言 它在**

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

# 其他SEO基本要求

## 粒子图

您需要一个站点图。 这里有一个简单的控制器, 动态生成一个 :

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

## 机器人.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 阅读器。

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

# 图像呢?

您可能想知道如何为每个文章生成独特的 OG 图像。 对于一个技术博客来说, 这可能不值得努力。 您的流量来自 :

- **谷歌搜索** - 描述比图像更重要
- **RSS 接口** - 无图像
- **黑客新闻/编辑** - 缩略图几乎看不见
- **直接链接** - 开发商共享 URL 开发商共享 URL

一致的品牌形象对识别来说是件好事。 传统形象的生成对于视觉内容、新闻网站或营销页面来说更为重要,因为它们在Facebook和Twitter上竞相点击。

如果您真的想要生成它们, 您可以使用 [图像共享](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";
}
```

但对于一个技术博客来说呢?

# 测试您的 SEO

## Google Rich 成果测试

使用使用 [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)。您也可以使用 URL 检查工具来查看谷歌如何看待您的页面并要求重新索引。

# 摘要摘要摘要

解决办法直截了当:

问题 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修补 修 修补 问题 修补 修 修补 修 修 修 修 修 修 修 修 修 修补 修 修 修补 修 修 修补 修 修 修 修 修 修 修 修 修 修 修 修 修 修 修 修补 修 修 修 修 修 修 修 修 修 修 修 修 修 修补 修
|---------|-----|
静态元说明 动态 `ViewBag.Description` 由内容自动生成 {
 `<link rel="canonical">` 到布局 
* 没有结构化数据 * * JSON-LD `BlogPosting` 每个柱子上都有计谋,
缺少文章元数据 `article:published_time`, `article:author`, `article:tag` 元标记 * 
固定页面描述 * * 家庭、博客、联系人、搜索页面的独特描述 *

关键的洞察力: **谷歌无法读懂你的心**。如果每页都有相同的描述,谷歌无法知道是什么使每页具有独特性和价值。告诉谷歌每页都是什么,你会得到更好的排名。

让Google几周后重新搜索你的网站,你应该看到改进。SEO是一个漫长的游戏,但这些基本原理是其他一切基础的基础。

# 继续阅读

- [Google SEO 启动指南](https://developers.google.com/search/docs/fundamentals/seo-starter-guide)
- [Schema.org 博客插图](https://schema.org/BlogPosting)
- [开放图表程序](https://ogp.me/)
- [Google Rich 成果测试](https://search.google.com/test/rich-results)
- [Google 搜索控制台](https://search.google.com/search-console)