动态

详情 返回 返回

Next的Seo實踐 - 动态 详情

第10章:Next的Seo實踐

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相關的開發與配置。
關於我

user avatar segmentfault 头像 ntksol 头像 pengxiaohei 头像
点赞 3 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.