- 前端
【免费下载链接】next-seo
Next SEO is a plug in that makes managing your SEO easier in Next.js projects.
本文是 next-seo 仓库中 CUSTOM_COMPONENTS.md 的技术深化解读。next-seo 内置了 Article、Product、Recipe、Event 等 30+ 个开箱即用的 JSON-LD 组件,但当业务需要
PodcastEpisode、RealEstateListing、Service、CourseInstance等 Schema.org 类型时,内置组件往往无法覆盖。此时可以利用 next-seo 暴露的核心工具JsonLdScript与 60+ 个processors处理器,以与内置组件完全一致的模式打造自定义结构化数据组件。
阅读本文后,你将掌握:如何使用JsonLdScript渲染结构化数据脚本、如何运用内置处理器处理"字符串/对象"多形态输入、如何编写带类型探测能力的自定义处理器,以及如何通过单元测试与 Google Rich Results Test 验证自定义组件的正确性。
目录
- 快速开始:几行代码创建一个自定义组件
- 核心概念:JsonLdScript、@type Optional 模式与 Processors
- 使用内置 Processors:四大类常见 Schema.org 类型
- 创建自定义 Processors
- 高级模式:嵌套处理、条件属性与多类型支持
- 最佳实践
- 真实项目示例:仓库中的四个自定义组件
- 测试你的自定义组件
- 从内联 JSON-LD 迁移到 next-seo 自定义组件
- Processor API 参考
快速开始:几行代码创建一个自定义组件
next-seo 在设计上把"渲染"与"数据处理"拆成了两个独立可复用的核心能力,这正是自定义组件的基石。一个最小可用的自定义组件只需要两步:构造结构化数据对象,然后交给JsonLdScript渲染。
import { JsonLdScript, processors } from "next-seo"; export function PodcastEpisodeJsonLd({ name, author, duration, url }) { const data = { "@context": "https://schema.org", "@type": "PodcastEpisode", name, ...(url && { url }), ...(duration && { duration }), ...(author && { author: processors.processAuthor(author) }), }; return <JsonLdScript data={data} scriptKey="podcast-episode" />; } // 使用方式 —— 使用者无需手写 @type! <PodcastEpisodeJsonLd name="Episode 1: Getting Started" author="Jane Doe" // 简单字符串即可! duration="PT30M" url="https://example.com/episode-1" />;注意两个关键点:
JsonLdScript与processors均由 next-seo 的公共入口 src/index.ts 统一导出,其中processors以命名空间形式导出全部处理函数,JsonLdScript为独立组件导出;- 自定义组件内部用
...(condition && { key: value })的条件展开语法控制可选属性,只有传入值时才输出对应字段,这与内置组件(如 ArticleJsonLd)的写法完全一致。
核心概念:JsonLdScript、@type Optional 模式与 Processors
1. JsonLdScript 组件:渲染层
JsonLdScript是所有结构化数据(无论内置还是自定义)的最终渲染出口。其源码位于 src/core/JsonLdScript.tsx,接收四个 props:
import { JsonLdScript } from "next-seo"; <JsonLdScript data={yourStructuredData} id={optionalId} // 可选:HTML id 属性 scriptKey={requiredKey} // 必填:React key,用于 script 元素 nonce={optionalNonce} // 可选:CSP nonce 值 />;从实现看,它内部做了三件重要的事:
- 空数据保护:当
data为null或undefined时直接返回null,不会渲染出空脚本标签(JsonLdScript.tsx); - 安全序列化:调用
stringify工具将数据转为 JSON 字符串。stringify(见 src/utils/stringify.ts)除了用safeJsonLdReplacer剔除null值,还会对</script>(大小写不敏感)、<!--、-->等危险序列做 Unicode 转义,防止结构化数据注入 HTML 时被解析器误判为脚本结束或注释——这是 W3C JSON-LD 规范对 script 元素内容的安全要求; - 默认 id 回退:未传
id时,脚本的 HTMLid会回退为scriptKey,同时data-testid也会被设为id,便于测试定位(JsonLdScript.tsx)。
其单元测试位于 src/core/JsonLdScript.test.tsx,覆盖了标签类型、序列化内容、id 回退以及空数据返回null等场景。
2. @type Optional 模式:核心设计原则
next-seo 的核心理念是:开发者永远不需要手动指定@type。这是通过"智能处理器"自动补全正确的 Schema.org 类型实现的。
// 使用方这样写: author="John Doe" // 处理器自动转换为: { "@type": "Person", name: "John Doe" }这一原则也体现在内置组件的类型定义上:ArticleJsonLdProps使用Omit<Article, "@type">这类工具类型,让使用者在 props 层面根本看不到@type字段(详见 ADDING_NEW_COMPONENTS.md 中关于@type的设计说明)。自定义组件应当继承并复刻这套模式。
3. Processors:灵活输入 → 规范化 Schema.org 对象
处理器(Processors)是一类把"灵活输入"转换成"带正确@type的 Schema.org 对象"的纯函数:
import { processors } from "next-seo"; const author = processors.processAuthor("John Doe"); const image = processors.processImage({ url: "image.jpg", width: 800 }); const address = processors.processAddress("123 Main St");几乎所有处理器都遵循相同的底层实现范式,即位于 src/utils/processors.ts 的通用工具函数processSchemaType:
export function processSchemaType<T extends { "@type": string }>( value: unknown, schemaType: string, stringHandler?: (str: string) => Omit<T, "@type">, numberHandler?: (num: number) => Omit<T, "@type">, ): T { // 字符串输入 + 提供 stringHandler → 按 handler 构造对象 // 数字输入 + 提供 numberHandler → 按 handler 构造对象 // 已带 @type 的对象 → 原样返回(forward compatible) // 普通对象 → 自动补上 @type 并展开 // 兜底 → 仅返回 { "@type": schemaType } }该函数在 src/utils/processors.ts 中实现,它决定了"灵活输入"的四条处理规则:
- 字符串可以配合
stringHandler被转换为带@type的对象(例如processAddress("123 Main St")会生成{ "@type": "PostalAddress", streetAddress: "123 Main St" }); - 数字可以配合
numberHandler转换(例如processNumberOfEmployees(500)会生成{ "@type": "QuantitativeValue", value: 500 }); - 已含
@type的对象会被原样保留,保证向前兼容; - 无
@type的普通对象会自动补上@type后展开,从而让使用者始终无需关心类型字段。
使用内置 Processors:四大类常见 Schema.org 类型
processors命名空间通过 src/utils/processors.export.ts 统一导出,按领域分为 People & Organizations、Media & Content、Locations & Places、Commerce & Offers、Reviews & Ratings、HowTo Content、Membership & Loyalty、Specifications & Values、Products、Education & Requirements、Data & Creative Works、Accommodation & Rental、Interaction & Statistics 等十余个类别,总数超过 60 个。下面按文档给出的四大类逐一展开。
人物与组织(People & Organizations)
import { processors } from "next-seo"; // 灵活的 author 输入 processors.processAuthor("Jane Doe"); // → { "@type": "Person", name: "Jane Doe" } processors.processAuthor({ name: "ACME Corp", logo: "..." }); // → { "@type": "Organization", ... } // 其他人物/组织处理器 processors.processPublisher("Tech Publishing"); processors.processOrganizer({ name: "Event Co", url: "..." }); processors.processPerformer("Band Name");processAuthor的实现(src/utils/processors.ts)展示了"智能类型探测"的典型逻辑:
- 字符串输入:通过内置的组织特征词表(magazine、publication、company、corp、inc、llc、ltd、group、foundation、institute、association、press、media、studio 等 20+ 个关键词)判断字符串更像组织还是个人,命中则输出
Organization,否则输出Person; - 已带
@type的对象:原样返回; - 普通对象:依据是否含有
logo、address、contactPoint等组织专属属性判定为Organization,并递归处理其嵌套的 logo、address、contactPoint 字段(processOrganizationFields辅助函数),否则默认为Person。
processPerformer与processOrganizer则通过检测familyName、givenName、additionalName这类 Person 专属属性来区分个人与团体/组织(src/utils/processors.ts)。
媒体与内容(Media & Content)
// 图片 —— 字符串 URL 或 ImageObject 对象 processors.processImage("https://example.com/image.jpg"); processors.processImage({ url: "...", width: 800, height: 600 }); // 视频 processors.processVideo({ name: "Tutorial", uploadDate: "2024-01-01", thumbnailUrl: "...", }); // 其他媒体处理器 processors.processLogo("logo.jpg"); processors.processScreenshot({ url: "...", caption: "App screenshot" });注意processImage的一个特殊约定:当输入是字符串 URL 时,它原样返回字符串(不包裹@type),只有对象输入才补上ImageObject类型(src/utils/processors.ts)。这是因为 Schema.org 允许image字段直接使用 URL 字符串,这样的输出更简洁。processLogo直接委托给processImage(src/utils/processors.ts)。该类别下还包括processVideo、processClip、processBroadcastEvent、processSeekToAction、processThreeDModel等。
位置与场所(Locations & Places)
// 简单字符串 → PostalAddress processors.processAddress("123 Main St, City, Country"); // 携带更多细节的对象 processors.processAddress({ streetAddress: "123 Main St", addressLocality: "San Francisco", addressRegion: "CA", postalCode: "94105", addressCountry: "US", }); // 带地理坐标的场所 processors.processPlace({ name: "Office", geo: { latitude: 37.7749, longitude: -122.4194 }, });processAddress借助processSchemaType的stringHandler参数,把字符串包装为{ "@type": "PostalAddress", streetAddress: str }(src/utils/processors.ts);processPlace的字符串处理则更复杂一些:字符串会同时生成name字段和一个内嵌的PostalAddress(src/utils/processors.ts)。此外还有processGeo、processJobLocation、processSpatialCoverage、processDefinedRegion等位置相关处理器。
商业与优惠(Commerce & Offers)
// 产品优惠 processors.processProductOffer({ price: 29.99, priceCurrency: "USD", availability: "https://schema.org/InStock", }); // 退货政策 processors.processMerchantReturnPolicy({ returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", merchantReturnDays: 30, });这类处理器不仅会补全@type,还会做归一化与嵌套处理。以processMerchantReturnPolicy为例(src/utils/processors.ts),它会:
- 把
applicableCountry、returnPolicyCountry、returnMethod、refundType、itemCondition等字段统一归一化为数组形式; - 用
processSimpleMonetaryAmount递归处理returnShippingFeesAmount、restockingFee等金额字段(支持数字输入,数字会默认带上currency: "USD",见 src/utils/processors.ts); - 对
returnPolicySeasonalOverride做批量处理。
该类别还包括processOffer、processAggregateOffer、processPriceSpecification、processUnitPriceSpecification、processMonetaryAmount、processOfferShippingDetails、processShippingDeliveryTime等。
创建自定义 Processors
当内置处理器无法覆盖业务形态时,你可以自己编写处理器,并自由组合内置工具。
基础自定义处理器
import { processors } from "next-seo"; // 为播客主持人定制的处理器 function processHost(host: string | { name: string; bio?: string }) { if (typeof host === "string") { return { "@type": "Person", name: host, }; } // 对象输入复用通用 helper return processors.processSchemaType(host, "Person"); } // 在组件中使用 export function PodcastJsonLd({ hosts, ...props }) { const data = { "@context": "https://schema.org", "@type": "PodcastSeries", ...(hosts && { host: Array.isArray(hosts) ? hosts.map(processHost) : processHost(hosts), }), }; return <JsonLdScript data={data} scriptKey="podcast" />; }这里展示了两个要点:字符串输入直接手工构造对象;对象输入则委托给processSchemaType(host, "Person")—— 该通用函数会自动为无@type的对象补上"@type": "Person",若对象已带@type则原样透传。
带类型探测的高级自定义处理器
当同一个输入可能对应多个 Schema.org 子类型时,可以根据属性做启发式探测:
function processCreativeWork(work: string | Record<string, any>) { if (typeof work === "string") { return { "@type": "CreativeWork", name: work, }; } // 已带 @type?原样返回 if (work["@type"]) { return work; } // 根据属性探测类型 let type = "CreativeWork"; if ("isbn" in work) type = "Book"; else if ("director" in work) type = "Movie"; else if ("artist" in work) type = "MusicRecording"; return { "@type": type, ...work, }; }这种"按属性探测 → 补全类型"的模式与内置processAuthor、processPerformer的实现思路一脉相承:先检查@type,再检查领域专属属性(logo/address/contactPoint→ Organization,familyName/givenName→ Person),最后落入默认类型兜底。
高级模式:嵌套处理、条件属性与多类型支持
嵌套处理(Nested Processing)
结构化数据经常是层层嵌套的,处理器可以在嵌套过程中逐层递归转换:
function processEventWithVenue(event: { name: string; venue?: string | { name: string; address?: string }; organizer?: string | { name: string }; }) { return { "@type": "Event", name: event.name, ...(event.venue && { location: typeof event.venue === "string" ? processors.processPlace(event.venue) : processors.processPlace({ ...event.venue, ...(event.venue.address && { address: processors.processAddress(event.venue.address), }), }), }), ...(event.organizer && { organizer: processors.processOrganizer(event.organizer), }), }; }注意嵌套层次:venue字符串走processPlace(生成Place+ 内嵌PostalAddress);venue对象则先把内部的address用processAddress处理成PostalAddress,再整体交给processPlace。内置处理器的嵌套逻辑同样如此——processReview会递归处理内部的reviewRating(→Rating)与author(→Person/Organization),见 src/utils/processors.ts。
条件属性(Conditional Properties)
只渲染有值的属性,避免输出空字段,是 next-seo 全仓库统一的书写风格:
export function CustomProductJsonLd({ name, description, price, image, brand, reviews, aggregateRating, ...props }) { const data = { "@context": "https://schema.org", "@type": "Product", name, ...(description && { description }), ...(price && { offers: { "@type": "Offer", price, priceCurrency: "USD", }, }), ...(image && { image: Array.isArray(image) ? image.map(processors.processImage) : processors.processImage(image), }), ...(brand && { brand: processors.processBrand(brand) }), ...(reviews && { review: Array.isArray(reviews) ? reviews.map(processors.processReview) : processors.processReview(reviews), }), ...(aggregateRating && { aggregateRating: processors.processAggregateRating(aggregateRating), }), }; return <JsonLdScript data={data} scriptKey={props.scriptKey || "product"} />; }两个值得注意的细节:
- 数组与单值的统一处理:
image、reviews都通过Array.isArray分支,单值走单个处理器,数组走.map()批量处理——这与 ArticleJsonLd 对author、image的处理方式一致; - scriptKey 回退:
scriptKey={props.scriptKey || "product"}允许调用方覆盖默认 key,避免同页面多个组件实例发生 React key 冲突。
多类型支持(Multiple Schema Types)
当同一个组件的输出可能属于多个相近的 Schema.org 类型时,用typeprop 让调用方选择:
type ScholarlyArticleType = | "ScholarlyArticle" | "MedicalScholarlyArticle" | "TechArticle"; export function ScholarlyArticleJsonLd({ type = "ScholarlyArticle", headline, author, datePublished, journal, doi, ...props }: { type?: ScholarlyArticleType; headline: string; author: string | Array<string | { name: string }>; datePublished: string; journal?: string; doi?: string; }) { const data = { "@context": "https://schema.org", "@type": type, headline, datePublished, ...(author && { author: Array.isArray(author) ? author.map(processors.processAuthor) : processors.processAuthor(author), }), ...(journal && { isPartOf: { "@type": "PublicationIssue", name: journal, }, }), ...(doi && { identifier: processors.processIdentifier(doi) }), }; return ( <JsonLdScript data={data} scriptKey={props.scriptKey || `article-${type}`} /> ); }这里@type直接取自typeprop(带默认值),author依然交给processAuthor做灵活输入处理,doi通过processIdentifier规范化。注意scriptKey依据 type 动态生成(article-${type}),确保同一页面渲染多个类型的实例时 key 不冲突。
最佳实践
1. 灵活类型一律走处理器
// ✅ 推荐 —— 使用处理器 author: processors.processAuthor(author) // ❌ 不推荐 —— 强迫使用者手写 @type author: { "@type": "Person", ...author }使用处理器意味着:调用方传字符串可以、传无@type对象可以、传已带@type对象也可以,全部分支都被正确收敛。
2. 同时支持数组与单值
// 支持单值与数组两种输入 ...(tags && { keywords: Array.isArray(tags) ? tags.join(', ') : tags })3. 应用合理的默认值
// dateModified 缺省时回退到 datePublished const data = { datePublished, dateModified: dateModified || datePublished, };这一惯例同样存在于内置组件中:ArticleJsonLd在未传dateModified但传了datePublished时会自动以datePublished填充dateModified(参见 ADDING_NEW_COMPONENTS.md 的默认值示例)。
4. 用 TypeScript 提升开发体验
interface ServiceJsonLdProps { name: string; provider?: string | Organization; areaServed?: string | string[]; serviceType?: string; scriptId?: string; scriptKey?: string; }5. 为组件编写文档注释
/** * ServiceJsonLd - Structured data for service offerings * * @example * <ServiceJsonLd * name="Web Development" * provider="Tech Agency" * areaServed={["US", "CA", "UK"]} * serviceType="Professional Service" * /> */ export function ServiceJsonLd({ ... }) { ... }真实项目示例:仓库中的四个自定义组件
为了证明这套方法论的可落地性,仓库在 examples/app-router-showcase/components/custom 下提供了 4 个完整的自定义组件实现,并配套了真实示例页面与端到端测试。
1. PodcastSeriesJsonLd:播客系列 + 单集列表
实现位于 examples/app-router-showcase/components/custom/PodcastSeriesJsonLd.tsx,演示页面为 examples/app-router-showcase/app/custom-podcast/page.tsx。组件:
host属性接受字符串或对象,统一经processors.processAuthor转换(字符串 "Sarah Johnson" →{ "@type": "Person", name: "Sarah Johnson" });image经processors.processImage处理,字符串 URL 原样保留;episodes数组逐项映射为PodcastEpisode子类型,并自动按index + 1生成position字段(如Episode 1→position: 1);- 文档中的同主题代码片段在此基础上还展示了
description、datePublished、url等可选字段的处理。
2. ServiceJsonLd:服务 + 提供方 + 评分
实现位于 examples/app-router-showcase/components/custom/ServiceJsonLd.tsx,演示页面为 examples/app-router-showcase/app/custom-service/page.tsx。该组件展示了三种能力的组合:
provider接受字符串或含logo、address等字段的复杂对象,统一经processors.processOrganization转换,嵌套的address会被自动升级为PostalAddress;areaServed统一归一化为数组(Array.isArray(areaServed) ? areaServed : [areaServed]);aggregateRating经processors.processAggregateRating补全AggregateRating类型。
3. 文档中的四个进阶示例
CUSTOM_COMPONENTS.md 还给出了四个可直接借鉴的完整组件模板:
- Podcast Series with Episodes:完整的
PodcastSeriesProps接口 + 单集position自动编号; - Real Estate Listing:
priceCurrency带"USD"默认值,images数组批量processImage,floorSize走processQuantitativeValue; - Service with Pricing Tiers:
pricingTiers映射为hasOfferCatalog→OfferCatalog→Offer层级,价格支持number或{ min, max }区间两种形态(区间走PriceSpecification+minPrice/maxPrice); - Educational Course with Modules:
modules映射为hasCourseInstance→CourseInstance(含courseMode: "online"与position),price存在时生成带validFrom/validThrough的Offer。
4. 仓库内的可验证事实
这些组件的实际渲染行为被 tests/e2e/customComponents.e2e.spec.ts 完整验证,例如:
- 字符串
host被转换为Person且带@type(第 25-28 行断言); - 字符串图片 URL 保持字符串形态不包装对象(第 30-31 行断言);
provider复杂对象被补上Organization,其嵌套address被补上PostalAddress(第 70-83 行断言);- 全部嵌套对象(
PodcastSeries、Person、PodcastEpisode、Service、Organization、PostalAddress、Offer、AggregateRating)都无需调用方手写@type(第 138-168 行断言); - 输出 JSON 可被
JSON.parse正常解析且包含@context、@type、name三个基础字段(第 170-194 行断言)。
此外,tests/e2e/jsonValidation.e2e.spec.ts 也针对这两个自定义组件补充了 JSON 合法性校验。
测试你的自定义组件
单元测试
自定义组件与内置组件共用完全相同的测试手段——渲染后从 DOM 中取出script[type="application/ld+json"],解析其文本内容并断言:
import { render } from "@testing-library/react"; import { ServiceJsonLd } from "./ServiceJsonLd"; describe("ServiceJsonLd", () => { it("renders service with basic props", () => { const { container } = render( <ServiceJsonLd name="Consulting Service" provider="Tech Solutions Inc" />, ); const script = container.querySelector( 'script[type="application/ld+json"]', ); const data = JSON.parse(script.textContent); expect(data["@type"]).toBe("Service"); expect(data.name).toBe("Consulting Service"); expect(data.provider["@type"]).toBe("Organization"); }); });这种测试模式与 src/core/JsonLdScript.test.tsx 以及src/components/下各内置组件的.test.tsx完全同构,自定义组件可以直接复用。
端到端验证
仓库的 E2E 规范要求测试必须走真实示例页面而非 mock 注入(详见 ADDING_NEW_COMPONENTS.md 的 E2E 指南):先在examples/app-router-showcase/app/下创建页面,再由 Playwright 脚本page.goto()导航并断言真实渲染输出。
搜索引擎验证
部署后使用 Google 的 Rich Results Test(search.google.com/test/rich-results)输入页面 URL 校验结构化数据,确认无错误、无警告。这是结构化数据上线前的最后一道质量门。
从内联 JSON-LD 迁移到 next-seo 自定义组件
如果你此前在页面里手写<script type="application/ld+json">,迁移到自定义组件后代码会更干净、类型更安全。
迁移前(内联 JSON-LD)
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ "@context": "https://schema.org", "@type": "Service", name: "My Service", provider: { "@type": "Organization", name: "My Company", }, }), }} />迁移后(自定义组件)
import { JsonLdScript, processors } from "next-seo"; export function ServiceJsonLd({ name, provider }) { const data = { "@context": "https://schema.org", "@type": "Service", name, provider: processors.processOrganization(provider), }; return <JsonLdScript data={data} scriptKey="service" />; } // 使用方式 —— 更简洁且类型安全! <ServiceJsonLd name="My Service" provider="My Company" // 无需 @type! />;迁移收益包括:不再手动JSON.stringify(stringify已内置安全转义)、不再写dangerouslySetInnerHTML、provider可以是任意灵活输入、整套输出可被 TypeScript 类型约束。
Processor API 参考
完整的处理器清单见 src/utils/processors.export.ts。核心处理器速查:
| 处理器 | 输入 → 输出 |
|---|---|
processSchemaType(value, type) | 任意输入 → 指定 Schema.org 类型的通用处理器 |
processAuthor(author) | 字符串/对象 →Person或Organization(带类型探测) |
processImage(image) | 字符串 URL 或ImageObject→ 字符串原样保留或ImageObject |
processAddress(address) | 字符串或PostalAddress→PostalAddress |
processPlace(place) | 字符串或Place→Place(含内嵌地址) |
processOffer(offer) | 对象 →Offer(含价格与 availability) |
processReview(review) | 对象 →Review(递归处理 rating 与 author) |
processAggregateRating(rating) | 对象 →AggregateRating(含评分与计数) |
| 其余 50+ 个 | 覆盖 HowTo、视频、课程、求职、数据目录、会员体系、住宿等领域的专用处理器 |
总结
基于 next-seo 创建自定义 JSON-LD 组件的完整流程:
- 从
next-seo导入JsonLdScript与processors; - 定义组件 props(推荐使用 TypeScript 联合类型描述灵活输入);
- 对灵活类型一律使用处理器转换;
- 遵循 @type Optional 模式,让调用方永远不用手写
@type; - 返回
<JsonLdScript data={data} scriptKey={...} />。
这套方法可以带来:TypeScript 全程类型安全、字符串/对象/数组多形态输入的统一收敛、调用方零@type样板代码、与内置组件完全一致的代码风格,以及易于单元测试和 E2E 验证的组件结构。
延伸阅读
- 内置组件的实现模式:查看 src/components 下的 30+ 个
*JsonLd.tsx组件及其同名.test.tsx - 在 next-seo 中新增一个内置组件的完整流程:阅读 ADDING_NEW_COMPONENTS.md
- 处理器底层实现与类型探测逻辑:src/utils/processors.ts
- 安全序列化与转义实现:src/utils/stringify.ts
- 自定义组件的真实运行样例:examples/app-router-showcase/components/custom 及对应 E2E 测试 tests/e2e/customComponents.e2e.spec.ts
- 前端
【免费下载链接】next-seo
Next SEO is a plug in that makes managing your SEO easier in Next.js projects.
相关推荐
Next SEO 开发者指南:JSON-LD 组件架构与「@type 可选」设计模式实战解析
Next SEO 开发者指南:JSON LD 组件架构与「@type 可选」设计模式实战解析 导读 next seo 是一个面向 Next.js 项目的 SEO
前端next-seo 仓库开发指南:命令工作流、项目架构与 @type Optional Pattern 设计模式解析
next seo 仓库开发指南:命令工作流、项目架构与 @type Optional Pattern 设计模式解析 导读 本文基于 next seo 仓库根目录
前端Next.js 中集成 next-seo:默认 SEO、Open Graph 与 JSON-LD 结构化数据的完整示例解析
Next.js 中集成 next seo:默认 SEO、Open Graph 与 JSON LD 结构化数据的完整示例解析 在 Next.js 应用中统一管理
前端后端Web框架SSR前端构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考