第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,我的猜測這是由於其設計和實現方式導致的。以下是幾個主要原因:
- 服務器端執行:
generateMetadata主要在服務器端執行,而 Suspense 主要用於客户端渲染中處理異步操作。 - 元數據的關鍵性:元數據對於SEO非常重要,Next優先考慮確保元數據在初始 HTML 中可用,而不是延遲加載。
- 渲染順序:元數據通常需要在頁面內容之前生成,因為它們位於 HTML 的
<head>部分。這使得難以將其納入 Suspense 的流式渲染模型中。 - 兼容性考慮:不是所有的客户端(如搜索引擎爬蟲)都能處理通過 JavaScript 動態插入的元數據。
這種設計導致了一些潛在的性能問題:
- 阻塞渲染:如果
generateMetadata函數執行時間較長,它會延遲整個頁面的渲染。 - 無法並行加載:元數據生成和頁面內容加載無法並行進行,可能會增加總體加載時間。
- 客户端導航延遲:在客户端導航時,新頁面的渲染可能會因為等待元數據生成而被延遲。
為了解決這些問題,Next引入了"流式元數據"(Streaming Metadata)功能。這個新特性旨在提高頁面加載速度,特別是在處理慢速元數據生成時,但只能在canary中使用。
流式元數據的主要優勢:
- 非阻塞渲染:
generateMetadata返回的元數據被視為可掛起的數據,允許頁面內容立即渲染。 - 異步注入:元數據在解析完成後,會在客户端異步注入到頁面中。
- SEO友好:對於搜索引擎爬蟲,仍然會在HTML中接收完全渲染的元數據。
- 用户體驗優先:對於人類用户,他們主要關心頁面內容,元數據可以稍後添加而不影響他們的體驗。
如何使用:
要啓用流式元數據功能,你需要在 next.config.js 中添加以下配置:
module.exports = {
experimental: {
streamingMetadata: true
}
}
注意事項:
- 這個功能默認是禁用的,需要手動開啓。
- 對於某些有限的機器人(如不能處理JavaScript的爬蟲),你可以使用
experimental.htmlLimitedBots選項來指定它們應該接收完全阻塞的元數據,但我目前的做法是用正則匹配了市面主流的所有爬蟲。 - 默認情況下,只有能夠像無頭瀏覽器一樣運行的Google機器人會在啓用此功能時接收流式元數據。
為什麼這個解決方案很重要:
- 性能提升:通過允許頁面內容先渲染,然後異步加載元數據,可以顯著提高感知加載速度。
- 更好的用户體驗:用户可以更快地看到和交互頁面內容,而不必等待所有元數據加載完成。
- 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)內,具有相同參數的重複請求會被自動去重。這意味着:
getData函數實際上只會被調用一次。- 第一次調用(通常是在
generateMetadata中)的結果會被緩存。 - 後續的調用(在頁面組件中)會直接使用緩存的結果,而不會觸發新的網絡請求。
2. robots.txt
2.1. robots.txt 的重要性和基本概念
robots.txt 文件是網站與搜索引擎爬蟲之間的一種通信機制。它位於網站的根目錄,作為網站管理員向搜索引擎爬蟲傳達爬取指令的第一道關卡。正確配置 robots.txt 可以:
- 指導爬蟲如何爬取網站內容
- 防止敏感或不必要的頁面被索引
- 優化網站的爬取效率
- 間接影響網站的 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'
}
}
這個例子展示了動態生成方法的強大之處:
- 我們可以為不同的 User-Agent 設置不同的規則。
- 可以輕鬆地添加多個 allow 和 disallow 規則。
- 除了 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 設置不同的規則。userAgent、allow和disallow都可以是字符串或字符串數組,方便設置多個值。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 的注意事項
-
使用通配符謹慎:
robots.txt 支持使用通配符,但要謹慎使用。錯誤的通配符可能會意外地阻止重要頁面被索引。例如:User-agent: * Disallow: /*.pdf這會阻止所有 PDF 文件被索引。
- 指定正確的 Sitemap 位置:
始終在 robots.txt 中包含你的 Sitemap 位置。這有助於搜索引擎更全面地發現和索引你的網站頁面。 -
考慮爬蟲預算:
對於大型網站,可以使用Crawl-delay指令來控制爬蟲的爬取頻率,以防止服務器過載:User-agent: * Crawl-delay: 10這告訴爬蟲在每次請求之間等待 10 秒。
3. Sitemaps
Sitemap 是一個 XML 文件,其中包含了網站上所有重要頁面的列表。它的主要目的是幫助搜索引擎更好地瞭解和索引網站的結構。正確配置和使用 Sitemap 可以:
- 提高網站的索引效率
- 確保重要頁面被搜索引擎發現和收錄
- 為大型或複雜的網站提供清晰的結構指引
- 間接提升網站的 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
}
]
}
這個方法的優勢在於:
- 可以動態生成 URL 列表,特別適合內容經常變化的網站。
- 可以輕鬆地從數據庫或 API 獲取最新的頁面信息。
- 可以根據不同的條件設置不同的優先級和更新頻率。
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
}>
-
Sitemap 的基本結構
SitemapFile是一個數組類型,表明一個 Sitemap 可以包含多個 URL 條目。- 每個條目都是一個對象,代表網站中的一個頁面。
-
必需信息
url: string: 這是唯一的必需字段。每個條目必須包含一個 URL,指向網站的特定頁面。
-
時間相關信息
lastModified?: string | Date | undefined: 可選字段,表示頁面的最後修改時間。可以是字符串(如 ISO 8601 格式)或 JavaScript Date 對象。
-
更新頻率
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined:
可選字段,指示頁面內容更新的預期頻率。這有助於搜索引擎決定多久重新爬取一次頁面。
-
頁面重要性
priority?: number | undefined: 可選字段,表示頁面相對於網站其他頁面的重要性。值範圍通常在 0.0 到 1.0 之間。
-
多語言支持
alternates?: { languages?: Languages<string> | undefined } | undefined:
可選字段,用於指定頁面的其他語言版本。這對於國際化網站特別有用。
-
多媒體支持
images?: string[] | undefined: 可選字段,允許指定與頁面相關的圖片 URL。videos?: Videos[] | undefined: 可選字段,允許包含與頁面相關的視頻信息。
通過這個類型定義,我們可以看出 Sitemap 不僅僅是簡單的 URL 列表,而是可以包含豐富的元數據信息。這些信息可以幫助搜索引擎更好地理解和索引網站內容:
- 它可以指導搜索引擎何時重新爬取頁面(通過
lastModified和changeFrequency)。 - 它可以提示搜索引擎頁面的相對重要性(通過
priority)。 - 它支持多語言網站的 SEO 優化(通過
alternates)。 - 它允許為圖片和視頻內容提供額外的 SEO 信息(通過
images和videos)。
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
}))
}
在這個例子中:
generateSitemaps函數根據產品總數計算需要多少個 sitemap 文件。- 它返回一個對象數組,每個對象都有一個
id屬性,代表一個 sitemap。 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 的最佳實踐和注意事項
- 保持更新:
確保你的 Sitemap 始終反映網站的最新結構和內容。對於動態生成的 Sitemap,考慮設置定期重新生成的機制。 - 遵守大小限制:
單個 Sitemap 文件不應超過 50,000 個 URL。如果你的網站超過這個限制,考慮使用 generateSitemaps 索引文件。 - 提交到搜索引擎:
主動將你的 Sitemap 提交到主要搜索引擎的網站管理工具中,如 Google Search Console。 - 使用正確的 URL:
確保 Sitemap 中的 URL 是規範的、可訪問的,並且與你網站上實際使用的 URL 一致。 - 不設置權重和更新頻率:
最好的方式就是不設置,google會自動計算頻率和權重 - 考慮多語言網站:
如果你的網站支持多種語言,考慮為每種語言版本創建單獨的 Sitemap,或使用 hreflang 標籤。 - 包含圖片和視頻信息:
對於圖片和視頻內容豐富的網站,考慮在 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相關的開發與配置。
關於我