SEO

    10.Next的Seo实践

    Published
    February 16, 2025
    Reading Time
    10 min read
    Author
    Felix

    1. Meta标签

    Next App Router比较主流的有两种定义源数据标签的方式,一种是通过在布局或者页面上导出一个 metadata 的对象,会自动生成对应的Meta源数据标签,这是静态的。

    而另外一种则是动态生成meta标签,这种场景通常需要先请求接口得到一些信息的动态源数据页面,在这种情况下我们采用generateMetadata函数。

    1.1. 静态Meta标签

    仅仅只需要在页面或者布局中添加这一段。

    export const metadata: Metadata = {
      metadataBase: new URL(APP_ORIGIN),
      title: APP_TITLE,
      description: APP_DESCRIPTION,
      creator: APP_NAME,
      icons: {
        icon: '/favicon.ico',
        shortcut: '/favicon.ico'
      },
      openGraph: {
        title: APP_TITLE,
        description: APP_DESCRIPTION,
        url: APP_ORIGIN,
        siteName: APP_NAME,
        images: [
          {
            url: OG_URL,
            width: 2880,
            height: 1800,
            alt: APP_NAME
          }
        ],
        type: 'website',
        locale: 'en_US'
      },
      twitter: {
        card: 'summary_large_image',
        site: TWITTER_SOCIAL_URL,
        title: APP_TITLE,
        description: APP_DESCRIPTION,
        images: {
          url: '/og.jpg',
          width: 2880,
          height: 1800,
          alt: APP_NAME
        }
      }
    }
    

    1.2. 生成的HTML Meta标签

    上面的 metadata 对象会被 Next.js 自动转换为相应的 HTML meta 标签。假设我们的应用配置如下:

    const APP_ORIGIN = 'https://example.com'
    const APP_TITLE = 'My Awesome App'
    const APP_DESCRIPTION = 'This is an awesome app built with Next.js'
    const APP_NAME = 'AwesomeApp'
    const OG_URL = 'https://example.com/og-image.jpg'
    const TWITTER_SOCIAL_URL = '@awesome_app'
    

    那么,生成的 HTML head 部分可能会包含以下 meta 标签:

    <head>
      <title>My Awesome App</title>
      <meta name="description" content="This is an awesome app built with Next.js" />
      <meta name="creator" content="AwesomeApp" />
      <link rel="icon" href="/favicon.ico" />
      <link rel="shortcut icon" href="/favicon.ico" />
    
      <!-- Open Graph tags -->
      <meta property="og:title" content="My Awesome App" />
      <meta property="og:description" content="This is an awesome app built with Next.js" />
      <meta property="og:url" content="https://example.com" />
      <meta property="og:site_name" content="AwesomeApp" />
      <meta property="og:image" content="https://example.com/og-image.jpg" />
      <meta property="og:image:width" content="2880" />
      <meta property="og:image:height" content="1800" />
      <meta property="og:image:alt" content="AwesomeApp" />
      <meta property="og:type" content="website" />
      <meta property="og:locale" content="en_US" />
    
      <!-- Twitter Card tags -->
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:site" content="@awesome_app" />
      <meta name="twitter:title" content="My Awesome App" />
      <meta name="twitter:description" content="This is an awesome app built with Next.js" />
      <meta name="twitter:image" content="https://example.com/og.jpg" />
      <meta name="twitter:image:width" content="2880" />
      <meta name="twitter:image:height" content="1800" />
      <meta name="twitter:image:alt" content="AwesomeApp" />
    </head>
    

    这些生成的 meta 标签包含了我们在 metadata 对象中定义的所有信息,包括基本的页面信息、Open Graph 标签和 Twitter Card 标签。这些标签可以极大地提升我们的网页在搜索引擎结果中的展示效果,以及在社交媒体平台上的分享效果。

    1.3. 动态Meta标签

    对于需要根据动态数据生成元数据的页面,我们可以使用generateMetadata函数。这种方法特别适用于博客文章、产品详情页面等内容随时间或用户输入变化的场景。

    示例代码:

    import type { Metadata } from 'next'
    
    type Props = {
      params: { id: string }
    }
    
    export async function generateMetadata({ params }: Props): Promise<Metadata> {
      // 从API获取数据
      const product = await fetch(`https://api.acme.com/products/${params.id}`).then((res) => res.json())
    
      return {
        title: product.name,
        description: product.description,
        openGraph: {
          title: `${product.name} - Acme Products`,
          description: product.description,
          images: [{ url: product.image }]
        }
      }
    }
    
    export default function Page({ params }: Props) {
      // ...
    }
    

    这个函数允许我们基于动态数据(如API响应)生成元数据,确保每个页面都有独特且相关的SEO信息。

    1.4 generateMetadata的流式渲染

    流式渲染指的就是不用等待整个ssr中的请求完毕再抛出document,通过 Transfer-Encoding: chunked 的请求头标识把整个document文档进行分块传输,来进行优化页面内容传输以及提升用户体验。

    generateMetadata 函数不会触发 Suspense,我的猜测这是由于其设计和实现方式导致的。以下是几个主要原因:

    1. 服务器端执行generateMetadata 主要在服务器端执行,而 Suspense 主要用于客户端渲染中处理异步操作。

    2. 元数据的关键性:元数据对于SEO非常重要,Next优先考虑确保元数据在初始 HTML 中可用,而不是延迟加载。

    3. 渲染顺序:元数据通常需要在页面内容之前生成,因为它们位于 HTML 的 <head> 部分。这使得难以将其纳入 Suspense 的流式渲染模型中。

    4. 兼容性考虑:不是所有的客户端(如搜索引擎爬虫)都能处理通过 JavaScript 动态插入的元数据。

    这种设计导致了一些潜在的性能问题:

    • 阻塞渲染:如果 generateMetadata 函数执行时间较长,它会延迟整个页面的渲染。
    • 无法并行加载:元数据生成和页面内容加载无法并行进行,可能会增加总体加载时间。
    • 客户端导航延迟:在客户端导航时,新页面的渲染可能会因为等待元数据生成而被延迟。

    为了解决这些问题,Next引入了"流式元数据"(Streaming Metadata)功能。这个新特性旨在提高页面加载速度,特别是在处理慢速元数据生成时,但只能在canary中使用。

    流式元数据的主要优势:

    1. 非阻塞渲染generateMetadata 返回的元数据被视为可挂起的数据,允许页面内容立即渲染。
    2. 异步注入:元数据在解析完成后,会在客户端异步注入到页面中。
    3. SEO友好:对于搜索引擎爬虫,仍然会在HTML中接收完全渲染的元数据。
    4. 用户体验优先:对于人类用户,他们主要关心页面内容,元数据可以稍后添加而不影响他们的体验。

    如何使用:

    要启用流式元数据功能,你需要在 next.config.js 中添加以下配置:

    module.exports = {
      experimental: {
        streamingMetadata: true
      }
    }
    

    注意事项:

    1. 这个功能默认是禁用的,需要手动开启。
    2. 对于某些有限的机器人(如不能处理JavaScript的爬虫),你可以使用 experimental.htmlLimitedBots 选项来指定它们应该接收完全阻塞的元数据,但我目前的做法是用正则匹配了市面主流的所有爬虫。
    3. 默认情况下,只有能够像无头浏览器一样运行的Google机器人会在启用此功能时接收流式元数据。

    为什么这个解决方案很重要:

    1. 性能提升:通过允许页面内容先渲染,然后异步加载元数据,可以显著提高感知加载速度。
    2. 更好的用户体验:用户可以更快地看到和交互页面内容,而不必等待所有元数据加载完成。
    3. SEO和用户体验的平衡:通过为搜索引擎爬虫提供完整的元数据,同时为人类用户优化加载速度,实现了SEO和用户体验的完美平衡。

    1.5 generateMetadata和页面组件的请求优化

    在使用 generateMetadata 和页面组件时,一个常见的担忧是可能会导致重复的数据请求。因为 generateMetadata 和页面组件可能需要相同的数据。

    请求重复问题

    考虑以下场景:

    import type { Metadata } from 'next'
    
    async function getData(id: string) {
      const res = await fetch(`https://api.example.com/product/${id}`)
      return res.json()
    }
    
    export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
      const product = await getData(params.id)
      return { title: product.name }
    }
    
    export default async function Page({ params }: { params: { id: string } }) {
      const product = await getData(params.id)
      return <h1>{product.name}</h1>
    }
    

    乍看之下,似乎 getData 函数会被调用两次:一次在 generateMetadata 中,另一次在页面组件中。

    Next.js 的请求去重优化

    但Next.js 已经内置了请求去重优化。在同一个路由段(route segment)内,具有相同参数的重复请求会被自动去重。这意味着:

    1. getData 函数实际上只会被调用一次。
    2. 第一次调用(通常是在 generateMetadata 中)的结果会被缓存。
    3. 后续的调用(在页面组件中)会直接使用缓存的结果,而不会触发新的网络请求。

    2. robots.txt

    2.1. robots.txt 的重要性和基本概念

    robots.txt 文件是网站与搜索引擎爬虫之间的一种通信机制。它位于网站的根目录,作为网站管理员向搜索引擎爬虫传达爬取指令的第一道关卡。正确配置 robots.txt 可以:

    1. 指导爬虫如何爬取网站内容
    2. 防止敏感或不必要的页面被索引
    3. 优化网站的爬取效率
    4. 间接影响网站的 SEO 表现

    在 Next.js 应用中,我们有两种方式来实现 robots.txt:静态文件方法和动态生成方法。每种方法都有其特定的使用场景和优势。

    2.2. 静态Robots.txt

    静态文件方法是最直观的实现方式。你只需在public/目录下创建一个名为 robots.txt 的文件。

    例如,一个基本的 robots.txt 文件可能如下所示:

    User-Agent: *
    Allow: /
    Disallow: /admin/
    Disallow: /private/
    Sitemap: https://www.yourwebsite.com/sitemap.xml
    

    让我们逐行解析这个文件:

    • User-Agent: *:这一行表示以下规则适用于所有的搜索引擎爬虫。
    • Allow: /:允许爬虫访问网站的所有页面(除非被后续规则覆盖)。
    • Disallow: /admin/:禁止爬虫访问 /admin/ 目录及其子目录。
    • Disallow: /private/:同样禁止爬虫访问 /private/ 目录及其子目录。
    • Sitemap: https://www.yourwebsite.com/sitemap.xml:指明网站 Sitemap 的位置,帮助搜索引擎更好地了解网站结构。

    静态文件方法的优点是简单直接,适合网站结构相对固定、不需要频繁更新 robots.txt 内容的情况。

    2.3. 动态生成

    Next.js 提供了一种通过代码动态生成 robots.txt 的方法

    app/ 目录下创建一个 robots.ts 文件:

    import { MetadataRoute } from 'next'
    
    export default function robots(): MetadataRoute.Robots {
      return {
        rules: [
          {
            userAgent: '*',
            allow: '/',
            disallow: ['/admin/', '/private/']
          },
          {
            userAgent: 'Googlebot',
            allow: '/admin/public-reports/',
            disallow: '/admin/'
          }
        ],
        sitemap: 'https://www.yourwebsite.com/sitemap.xml',
        host: 'https://www.yourwebsite.com'
      }
    }
    

    这个例子展示了动态生成方法的强大之处:

    1. 我们可以为不同的 User-Agent 设置不同的规则。
    2. 可以轻松地添加多个 allow 和 disallow 规则。
    3. 除了 sitemap,我们还可以指定 host。

    动态生成的结果将类似于:

    User-agent: *
    Allow: /
    Disallow: /admin/
    Disallow: /private/
    
    User-agent: Googlebot
    Allow: /admin/public-reports/
    Disallow: /admin/
    
    Sitemap: https://www.yourwebsite.com/sitemap.xml
    Host: https://www.yourwebsite.com
    

    动态生成,是一个编译时操作,就是打包的时候就会调用接口生成好,并不会影响爬虫访问sitemap的速度。

    2.4. 从类型定义理解Robot

    我们通过TypeScript的类型定义去理解Robots

    type RobotsFile = {
      rules:
        | {
            userAgent?: string | string[] | undefined
            allow?: string | string[] | undefined
            disallow?: string | string[] | undefined
            crawlDelay?: number | undefined
          }
        | Array<{
            userAgent: string | string[]
            allow?: string | string[] | undefined
            disallow?: string | string[] | undefined
            crawlDelay?: number | undefined
          }>
      sitemap?: string | string[] | undefined
      host?: string | undefined
    }
    

    这个类型定义告诉我们:

    • rules 可以是一个对象或对象数组,允许你为不同的 User-Agent 设置不同的规则。
    • userAgentallowdisallow 都可以是字符串或字符串数组,方便设置多个值。
    • crawlDelay 是一个可选的数字,用于指定爬虫在两次请求之间应该等待的秒数。
    • sitemap 可以是单个 URL 或 URL 数组,允许指定多个 Sitemap。
    • host 是一个可选字段,用于指定网站的首选域名。

    2.5. 动态生成

    动态生成 robots.txt 的方法不仅灵活,还允许我们根据不同的条件生成不同的内容。例如:

    import { MetadataRoute } from 'next'
    
    export default function robots(): MetadataRoute.Robots {
      const isProduction = process.env.NODE_ENV === 'production'
    
      return {
        rules: {
          userAgent: '*',
          allow: '/',
          disallow: isProduction ? [] : ['/']
        },
        sitemap: 'https://www.yourwebsite.com/sitemap.xml',
        host: 'https://www.yourwebsite.com'
      }
    }
    

    在这个例子中,我们根据环境变量动态决定是否允许搜索引擎爬取网站。在生产环境中,我们允许爬取所有内容;而在非生产环境中,我们禁止爬取任何内容。这种方法特别适用于防止测试或开发环境的网站被搜索引擎索引。

    2.6. robots.txt 的注意事项

    1. 使用通配符谨慎: robots.txt 支持使用通配符,但要谨慎使用。错误的通配符可能会意外地阻止重要页面被索引。例如:

      User-agent: *
      Disallow: /*.pdf
      

      这会阻止所有 PDF 文件被索引。

    2. 指定正确的 Sitemap 位置: 始终在 robots.txt 中包含你的 Sitemap 位置。这有助于搜索引擎更全面地发现和索引你的网站页面。

    3. 考虑爬虫预算: 对于大型网站,可以使用 Crawl-delay 指令来控制爬虫的爬取频率,以防止服务器过载:

      User-agent: *
      Crawl-delay: 10
      

      这告诉爬虫在每次请求之间等待 10 秒。

    3. Sitemaps

    Sitemap 是一个 XML 文件,其中包含了网站上所有重要页面的列表。它的主要目的是帮助搜索引擎更好地了解和索引网站的结构。正确配置和使用 Sitemap 可以:

    1. 提高网站的索引效率
    2. 确保重要页面被搜索引擎发现和收录
    3. 为大型或复杂的网站提供清晰的结构指引
    4. 间接提升网站的 SEO 表现

    在 Next.js 应用中,我们同样有两种方式来实现 Sitemap:静态和动态。

    3.1. 静态 Sitemap

    静态 Sitemap 方法适用于内容相对固定的小型网站。你只需在 public/ 目录下创建一个名为 sitemap.xml 的文件。

    一个基本的 sitemap.xml 文件可能如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <url>
        <loc>https://www.yourwebsite.com/</loc>
        <lastmod>2025-06-01</lastmod>
        <changefreq>daily</changefreq>
        <priority>1.0</priority>
      </url>
      <url>
        <loc>https://www.yourwebsite.com/about</loc>
        <lastmod>2023-05-15</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.8</priority>
      </url>
    </urlset>
    

    让我们解析这个文件的结构:

    • <urlset>: 这是 Sitemap 的根元素,包含了命名空间声明。
    • <url>: 每个 URL 条目都包含在这个标签内。
    • <loc>: 页面的完整 URL。
    • <lastmod>: 页面最后修改的日期。
    • <changefreq>: 页面内容更新的频率(可选)。
    • <priority>: 相对于网站其他页面的优先级(可选,范围 0.0 到 1.0)。

    3.2. 动态生成 Sitemap

    Next.js 提供了一种通过代码动态生成 Sitemap 的方法,这对于大型或经常更新内容的网站特别有用。

    app/ 目录下创建一个 sitemap.ts 文件:

    import { MetadataRoute } from 'next'
    
    export default function sitemap(): MetadataRoute.Sitemap {
      return [
        {
          url: 'https://www.yourwebsite.com',
          lastModified: new Date(),
          changeFrequency: 'yearly',
          priority: 1
        },
        {
          url: 'https://www.yourwebsite.com/about',
          lastModified: new Date(),
          changeFrequency: 'monthly',
          priority: 0.8
        },
        {
          url: 'https://www.yourwebsite.com/blog',
          lastModified: new Date(),
          changeFrequency: 'weekly',
          priority: 0.5
        }
      ]
    }
    

    这个方法的优势在于:

    1. 可以动态生成 URL 列表,特别适合内容经常变化的网站。
    2. 可以轻松地从数据库或 API 获取最新的页面信息。
    3. 可以根据不同的条件设置不同的优先级和更新频率。

    3.3. 从类型定义理解 Sitemap

    通过分析 SitemapFile 的 TypeScript 类型定义,我们可以深入理解 Sitemap 的结构和功能:

    type SitemapFile = Array<{
      url: string
      lastModified?: string | Date | undefined
      changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined
      priority?: number | undefined
      alternates?:
        | {
            languages?: Languages<string> | undefined
          }
        | undefined
      images?: string[] | undefined
      videos?: Videos[] | undefined
    }>
    
    1. Sitemap 的基本结构

      • SitemapFile 是一个数组类型,表明一个 Sitemap 可以包含多个 URL 条目。
      • 每个条目都是一个对象,代表网站中的一个页面。
    2. 必需信息

      • url: string: 这是唯一的必需字段。每个条目必须包含一个 URL,指向网站的特定页面。
    3. 时间相关信息

      • lastModified?: string | Date | undefined: 可选字段,表示页面的最后修改时间。可以是字符串(如 ISO 8601 格式)或 JavaScript Date 对象。
    4. 更新频率

      • changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined: 可选字段,指示页面内容更新的预期频率。这有助于搜索引擎决定多久重新爬取一次页面。
    5. 页面重要性

      • priority?: number | undefined: 可选字段,表示页面相对于网站其他页面的重要性。值范围通常在 0.0 到 1.0 之间。
    6. 多语言支持

      • alternates?: { languages?: Languages<string> | undefined } | undefined: 可选字段,用于指定页面的其他语言版本。这对于国际化网站特别有用。
    7. 多媒体支持

      • images?: string[] | undefined: 可选字段,允许指定与页面相关的图片 URL。
      • videos?: Videos[] | undefined: 可选字段,允许包含与页面相关的视频信息。

    通过这个类型定义,我们可以看出 Sitemap 不仅仅是简单的 URL 列表,而是可以包含丰富的元数据信息。这些信息可以帮助搜索引擎更好地理解和索引网站内容:

    • 它可以指导搜索引擎何时重新爬取页面(通过 lastModifiedchangeFrequency)。
    • 它可以提示搜索引擎页面的相对重要性(通过 priority)。
    • 它支持多语言网站的 SEO 优化(通过 alternates)。
    • 它允许为图片和视频内容提供额外的 SEO 信息(通过 imagesvideos)。

    3.4. 动态生成

    动态生成 Sitemap 的方法允许我们根据不同的条件生成不同的内容。例如:

    import { MetadataRoute } from 'next'
    
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
      // 从数据库或 API 获取博客文章列表
      const posts = await fetchBlogPosts()
    
      const blogUrls = posts.map((post) => ({
        url: `https://www.yourwebsite.com/blog/${post.slug}`,
        lastModified: post.updatedAt,
        changeFrequency: 'weekly' as const,
        priority: 0.7
      }))
    
      return [
        {
          url: 'https://www.yourwebsite.com',
          lastModified: new Date(),
          changeFrequency: 'yearly',
          priority: 1
        },
        ...blogUrls
      ]
    }
    

    但值得注意的是高版本默认静态渲染,如果要退出静态渲染可以这样,在请求这个sitemaps的时候就从编译时变成运行时请求了,不需要每次重新打包。

    import { MetadataRoute } from 'next'
    import { unstable_noStore as noStore } from 'next/cache'
    
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
      noStore()
      // 从数据库或 API 获取博客文章列表
      const posts = await fetchBlogPosts()
    
      const blogUrls = posts.map((post) => ({
        url: `https://www.yourwebsite.com/blog/${post.slug}`,
        lastModified: post.updatedAt,
        changeFrequency: 'weekly' as const,
        priority: 0.7
      }))
    
      return [
        {
          url: 'https://www.yourwebsite.com',
          lastModified: new Date(),
          changeFrequency: 'yearly',
          priority: 1
        },
        ...blogUrls
      ]
    }
    

    3.5. 使用 generateSitemaps 处理大型网站

    对于拥有成千上万个页面的大型网站来说,使用单一的 sitemap 文件可能不够用。Next.js 提供了 generateSitemaps 函数来创建多个 sitemap 文件,这在我们的网站超过 50,000 个 URL(单个 sitemap 文件的限制)时特别有用。

    以下是如何使用 generateSitemaps 的示例:

    import { MetadataRoute } from 'next'
    
    export async function generateSitemaps() {
      // 获取产品总数
      const totalProducts = await getTotalProductCount()
    
      // 计算需要的 sitemap 数量(假设每个 sitemap 包含 50,000 个 URL)
      const sitemapCount = Math.ceil(totalProducts / 50000)
    
      // 返回一个包含 sitemap id 的数组
      return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }))
    }
    
    export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
      // 计算这个 sitemap 的范围
      const start = id * 50000
      const end = start + 50000
    
      // 获取这个范围内的产品
      const products = await getProducts(start, end)
    
      // 生成 sitemap 条目
      return products.map((product) => ({
        url: `https://www.yourwebsite.com/product/${product.id}`,
        lastModified: product.updatedAt,
        changeFrequency: 'daily',
        priority: 0.7
      }))
    }
    

    在这个例子中:

    1. generateSitemaps 函数根据产品总数计算需要多少个 sitemap 文件。
    2. 它返回一个对象数组,每个对象都有一个 id 属性,代表一个 sitemap。
    3. sitemap 函数然后使用这个 id 来生成特定范围内产品的 sitemap。

    生成的 sitemap 文件将可以通过类似 /sitemap/[id].xml 的 URL 访问(例如,/sitemap/0.xml/sitemap/1.xml 等)。

    这种方法允许我们通过将 URL 分割到多个 sitemap 文件中来高效地管理大量 URL。它特别适用于电子商务网站、大型博客或任何具有大量动态生成页面的网站。

    记得还要创建一个 sitemap 索引文件,列出所有这些单独的 sitemap 文件,这样可以让搜索引擎更容易发现和爬取我们的所有内容。

    使用 generateSitemaps 可以帮助我们克服单个 sitemap 文件的 URL 数量限制,确保我们的大型网站能够被搜索引擎完全索引,从而提高网站的可见性和搜索引擎优化效果。

    3.6. Sitemap 的最佳实践和注意事项

    1. 保持更新: 确保你的 Sitemap 始终反映网站的最新结构和内容。对于动态生成的 Sitemap,考虑设置定期重新生成的机制。

    2. 遵守大小限制: 单个 Sitemap 文件不应超过 50,000 个 URL。如果你的网站超过这个限制,考虑使用 generateSitemaps 索引文件。

    3. 提交到搜索引擎: 主动将你的 Sitemap 提交到主要搜索引擎的网站管理工具中,如 Google Search Console。

    4. 使用正确的 URL: 确保 Sitemap 中的 URL 是规范的、可访问的,并且与你网站上实际使用的 URL 一致。

    5. 不设置权重和更新频率: 最好的方式就是不设置,google会自动计算频率和权重

    6. 考虑多语言网站: 如果你的网站支持多种语言,考虑为每种语言版本创建单独的 Sitemap,或使用 hreflang 标签。

    7. 包含图片和视频信息: 对于图片和视频内容丰富的网站,考虑在 Sitemap 中包含这些媒体资源的信息,以帮助它们在图片和视频搜索结果中出现。

    4. ld+json

    在 Next.js 项目中,我们可以直接使用 schema-dts 库来保证类型。

    4.1 使用 schema-dts 定义 ld+json

    首先,确保已经安装了 schema-dts

    npm install schema-dts
    

    然后,在我们的组件或布局文件中,可以这样使用:

    import { Organization, WithContext } from 'schema-dts'
    import { APP_NAME, APP_ORIGIN } from '@/constants'
    
    const jsonLd: WithContext<Organization> = {
      '@context': 'https://schema.org',
      '@type': 'Organization',
      name: APP_NAME,
      url: APP_ORIGIN,
      logo: `${APP_ORIGIN}/opengraph.jpg`,
      sameAs: [
        // 可以根据需要添加更多社交媒体链接
      ]
    }
    

    4.2 在页面中嵌入 ld+json

    在我们的页面或布局组件中,可以这样嵌入 JSON-LD:

    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
            <script
              type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
            />
          <body>{children}</body>
        </html>
      )
    }
    

    4.3 常用的 ld+json 富文本类型

    4.3.1 Organization(组织)

    适用于公司、机构或组织的网站。

    File: /app/layout.tsx

    import { Organization, WithContext } from 'schema-dts'
    
    const organizationJsonLd: WithContext<Organization> = {
      '@context': 'https://schema.org',
      '@type': 'Organization',
      name: 'Your Company Name',
      url: 'https://www.yourcompany.com',
      logo: 'https://www.yourcompany.com/logo.png',
      sameAs: [
        'https://www.facebook.com/yourcompany',
        'https://www.twitter.com/yourcompany',
        'https://www.linkedin.com/company/yourcompany'
      ]
    }
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head>
            <script
              type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
            />
          </head>
          <body>{children}</body>
        </html>
      )
    }
    

    4.3.2 LocalBusiness(本地商业)

    适用于有实体店面的本地商业。

    File: /app/about/page.tsx

    import { LocalBusiness, WithContext } from 'schema-dts'
    
    const localBusinessJsonLd: WithContext<LocalBusiness> = {
      '@context': 'https://schema.org',
      '@type': 'LocalBusiness',
      name: 'Your Local Business Name',
      image: 'https://example.com/photo-of-business.jpg',
      '@id': 'https://example.com',
      url: 'https://www.example.com',
      telephone: '+1-401-555-1212',
      address: {
        '@type': 'PostalAddress',
        streetAddress: '123 Main St',
        addressLocality: 'Anytown',
        addressRegion: 'ST',
        postalCode: '12345',
        addressCountry: 'US'
      },
      geo: {
        '@type': 'GeoCoordinates',
        latitude: 40.75,
        longitude: -73.98
      },
      openingHoursSpecification: [
        {
          '@type': 'OpeningHoursSpecification',
          dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
          opens: '09:00',
          closes: '17:00'
        }
      ]
    }
    
    export default function AboutPage() {
      return (
        <>
          <script
            type="application/ld+json"
            dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessJsonLd) }}
          />
          <h1>About Our Business</h1>
          {/* 其他页面内容 */}
        </>
      )
    }
    

    4.3.3 Article(文章)

    适用于博客文章或新闻报道。

    File: /app/blog/[slug]/page.tsx

    import { Article, WithContext } from 'schema-dts'
    
    const articleJsonLd: WithContext<Article> = {
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: 'Article Title',
      image: 'https://example.com/article-image.jpg',
      author: {
        '@type': 'Person',
        name: 'John Doe'
      },
      publisher: {
        '@type': 'Organization',
        name: 'Example Publisher',
        logo: {
          '@type': 'ImageObject',
          url: 'https://example.com/publisher-logo.jpg'
        }
      },
      datePublished: '2025-06-12',
      dateModified: '2025-06-13'
    }
    
    export default function BlogPost() {
      return (
        <>
          <script
            type="application/ld+json"
            dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
          />
          <h1>Article Title</h1>
          {/* 文章内容 */}
        </>
      )
    }
    

    4.3.4 Product(产品)

    适用于电子商务网站的产品页面。

    File: /app/products/[id]/page.tsx

    import { Product, WithContext } from 'schema-dts'
    
    const productJsonLd: WithContext<Product> = {
      '@context': 'https://schema.org',
      '@type': 'Product',
      name: 'Executive Anvil',
      image: 'https://example.com/photos/1x1/photo.jpg',
      description: 'Sleeker than ACME\'s Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.',
      sku: '0446310786',
      mpn: '925872',
      brand: {
        '@type': 'Brand',
        name: 'ACME'
      },
      review: {
        '@type': 'Review',
        reviewRating: {
          '@type': 'Rating',
          ratingValue: '4',
          bestRating: '5'
        },
        author: {
          '@type': 'Person',
          name: 'Fred Benson'
        }
      },
      aggregateRating: {
        '@type': 'AggregateRating',
        ratingValue: '4.4',
        reviewCount: '89'
      },
      offers: {
        '@type': 'Offer',
        url: 'https://example.com/anvil',
        priceCurrency: 'USD',
        price: '119.99',
        priceValidUntil: '2020-11-20',
        itemCondition: 'https://schema.org/UsedCondition',
        availability: 'https://schema.org/InStock'
      }
    }
    
    export default function ProductPage() {
      return (
        <>
          <script
            type="application/ld+json"
            dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
          />
          <h1>Executive Anvil</h1>
          {/* 产品详情 */}
        </>
      )
    }
    

    ld-json的实际意义就是在搜索结果中展示更吸引人的一部分,提高点击率和转化率。

    总结

    这就是Next中常用和主流与Seo相关的开发与配置。