head.tsx 就是一个 React 组件:用 loader 数据动态生成 SEO meta

看看大部分框架怎么处理 <head>

tsx 复制代码
// Next.js
export const metadata = {
  title: 'Blog Post',
  description: '...',
  openGraph: { title: '...', images: [...] },
}

// Remix
export const meta: MetaFunction = ({ data }) => [
  { title: 'Blog Post' },
  { name: 'description', content: '...' },
  { property: 'og:image', content: data.post.coverImage },
]

元数据是配置对象。你把字符串和键值对塞进框架规定的 schema,框架再把它们转成 HTML 标签。

Pareto 反其道而行。在 Pareto 里,head.tsx 是一个返回 JSX 的 React 组件:

tsx 复制代码
// app/head.tsx
export default function Head() {
  return (
    <>
      <title>My App</title>
      <meta name="description" content="My awesome app." />
    </>
  )
}

就这样。没有要学的 config schema,没有特殊的 MetaDescriptor 类型。你写 <title><meta>,React 19 自动把它们吊到文档 <head> 里。

本文讲清楚为什么这个设计更好,以及它在动态 SEO 上能解锁什么。

为什么组件比配置好

三个理由。

1. 你拿到了 JSX ------ 包括表达式、循环、条件

配置对象是静态数据。如果你想"只在用户是高级账号时加这条 meta",你要么在 return 前命令式地构造对象,要么把条件逻辑塞进值里。

组件是代码。条件按正常方式写:

tsx 复制代码
export default function Head({ loaderData }: HeadProps) {
  const data = loaderData as LoaderData
  return (
    <>
      <title>{data.product.name}</title>
      <meta name="description" content={data.product.tagline} />

      {data.product.coverImage && (
        <meta property="og:image" content={data.product.coverImage} />
      )}

      {data.product.keywords.map((kw) => (
        <meta property="article:tag" content={kw} key={kw} />
      ))}
    </>
  )
}

循环、守卫、条件渲染 ------ React 本来就做的事情。

2. Head 组件和你应用的其他部分一样能组合

想把共享的 OG 标签抽成 helper?它就是个 React 组件:

tsx 复制代码
function OpenGraphTags({ title, description, image }: OGProps) {
  return (
    <>
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={image} />
      <meta property="og:type" content="article" />
    </>
  )
}

export default function Head({ loaderData }: HeadProps) {
  const { post } = loaderData as { post: Post }
  return (
    <>
      <title>{post.title} --- My Blog</title>
      <OpenGraphTags
        title={post.title}
        description={post.excerpt}
        image={post.coverImage}
      />
    </>
  )
}

在配置对象的世界里,这是一个返回数组、然后 spread 到另一个数组里的 helper 函数。在这里,它是组件。读树就能看到 <head> 里最终会有什么 HTML。

3. React 19 帮你做了 hoisting

这才是让整个方案成立的关键特性。在 React 19 里,你在树里任何地方渲染的 <title><meta><link>,都会被吊到文档 <head> 里------SSR 和客户端导航都一样。没有框架特定的 MetaProvider 在收集和序列化元数据。这是 React 平台级特性。

路由树决定谁胜出

Head 组件从根渲染到页面。每一层贡献自己的标签。当两层渲染同一个标签(比如两个 <title>),浏览器用最后一个------最深路由的自动胜出。

csharp 复制代码
app/
  head.tsx                  ← 站点默认
  blog/
    [slug]/
      head.tsx              ← 单篇博文覆盖

根层设默认。叶子路由覆盖。这就是你思考 SEO 的方式------大部分标签全站通用,单页加自己的特定项。

tsx 复制代码
// app/head.tsx ------ 站点默认
export default function Head() {
  return (
    <>
      <title>My App</title>
      <meta name="description" content="The best app for doing things." />
      <link rel="icon" href="/favicon.ico" />
      <meta property="og:site_name" content="My App" />
    </>
  )
}
tsx 复制代码
// app/blog/[slug]/head.tsx ------ 单篇博文覆盖
import type { HeadProps } from '@paretojs/core'

export default function Head({ loaderData }: HeadProps) {
  const { post } = loaderData as { post: BlogPost }
  return (
    <>
      <title>{post.title} --- My App</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <meta property="og:image" content={post.coverImage} />
      <link rel="canonical" href={`https://myapp.com/blog/${post.slug}`} />
    </>
  )
}

HeadProps:带类型的 loader 数据

每个 head 组件收两个 prop:

tsx 复制代码
interface HeadProps {
  loaderData: unknown
  params: Record<string, string>
}

loaderData 是这个路由 loader 返回的东西。它被声明为 unknown------转成你的实际类型就行。

这就是让动态 SEO 水到渠成的关键。Loader 拉到了 post。Head 组件收到完全相同的数据。没有单独的 generateMetadata 调用去重新拉 post。数据流是:loader → page + head,两者用同一个结果渲染。

完整的动态 SEO 示例

给商品目录做实打实的每页 SEO 长这样。

tsx 复制代码
// app/products/[id]/head.tsx
import type { HeadProps } from '@paretojs/core'

export default function Head({ loaderData }: HeadProps) {
  const { product } = loaderData as { product: Product }
  const canonicalUrl = `https://shop.example.com/products/${product.id}`
  const primaryImage = product.images[0]?.url ?? '/default-og.png'

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images.map((img) => img.url),
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: product.currency,
      availability: product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
      url: canonicalUrl,
    },
  }

  return (
    <>
      <title>{`${product.name} --- Our Shop`}</title>
      <meta name="description" content={product.description} />
      <link rel="canonical" href={canonicalUrl} />

      <meta property="og:type" content="product" />
      <meta property="og:title" content={product.name} />
      <meta property="og:description" content={product.description} />
      <meta property="og:image" content={primaryImage} />

      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={product.name} />
      <meta name="twitter:image" content={primaryImage} />

      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
    </>
  )
}

一个文件。动态 title、完整 Open Graph、Twitter 卡片、canonical URL、JSON-LD 结构化数据------全部来自页面组件同样要用的那个 product 对象。没有重复拉取,没有单独的 metadata API。

简短版本

Pareto 的 head 系统是架在 React 19 特性之上的一个约定:

  • head.tsx 是一个返回 JSX 的 React 组件
  • React 19 自动把 <title><meta><link> 吊到 <head>
  • Head 组件把 loaderDataparams 作为 props 收到
  • 树从根渲染到页面,某种标签的最后一个胜出

没有独立的 metadata API 要学。你会 React,就会写 meta。任何动态场景,模式都一样:loader 返回数据,head.tsx 用它渲染 JSX,React 19 吊标签。SEO 搞定。

bash 复制代码
npx create-pareto@latest my-app
cd my-app && npm install && npm run dev

Pareto 是一个基于 Vite 的轻量流式优先 React SSR 框架。文档

相关推荐
Hello--_--World2 小时前
ES16:Set 集合方法增强、Promise.try、迭代器助手、JSON 模块导入 相关知识
开发语言·javascript·json
lemon_yyds2 小时前
Element UI 实践踩坑- date-picker 组件 定制化type="daterange"
前端·css
Alice-YUE2 小时前
ai对话平台中的functioncalling+mcp
前端·笔记·学习·语言模型
MXN_小南学前端2 小时前
Vue 视频上传实战:视频预览、MediaRecorder 压缩与自定义上传
前端·vue.js
programhelp_2 小时前
WeRide OA 2026 高频真题分享 & 详细备战指南
经验分享·算法·面试·职场和发展
Hilaku2 小时前
AI 生成的代码都是一坨屎?聊聊怎么给 Agent 制定工程约束
前端·javascript·ai编程
吴声子夜歌2 小时前
Vue3——使用Vue Router实现路由
前端·javascript·vue.js·vue-router
烛衔溟3 小时前
TypeScript 函数重载(Overloads)
javascript·ubuntu·typescript
CDwenhuohuo3 小时前
小程序全局使用api
javascript·vue.js·小程序