TemplateCore
00 / 00

SEO 优化

站点元信息、Open Graph、JSON-LD 结构化数据与 sitemap 配置

概览

@repo/seo 提供一组 TanStack Start SEO 工具,统一管理页面标题、描述、Open Graph、Twitter Card 和 JSON-LD 结构化数据。

  • 生成 TanStack Start route head() 对象
  • 配置站点级名称、域名、作者和默认语言
  • 输出 Open Graph 和 Twitter Card
  • 安全输出 JSON-LD script tag

环境变量

变量名说明示例
VITE_WEB_URL站点完整 URL(含协议,不带尾部斜杠)https://yourdomain.com

站点名称、默认描述、作者、关键词和 Open Graph 语言映射维护在 products/01mvp/apps/web/src/config/app.config.ts。SEO helper 读取这份配置,不再单独新增 VITE_SITE_NAME 这类环境变量。

如果你要改成自己的产品文案,可以把默认描述换成类似「面向独立开发者的 AI 产品实战模板」这种一句话定位。

站点元信息

定义站点配置

应用层配置在 products/01mvp/apps/web/src/shared/lib/seo.ts,它把通用包 @repo/seo 和当前站点配置接起来:

import type { TanStackStartSeoSite } from "@repo/seo";

import { appConfig } from "@/config/app.config";

export const seoSite = {
  applicationName: appConfig.site.longName,
  baseUrl: appConfig.site.baseUrl,
  defaultDescription: appConfig.site.description,
  defaultImages: [
    {
      alt: appConfig.site.longName,
      height: 630,
      type: "image/png",
      url: `${appConfig.site.baseUrl}/og/index.png`,
      width: 1200,
    },
  ],
  defaultTitle: appConfig.site.defaultTitle,
  siteName: appConfig.site.longName,
  titleTemplate: `%s | ${appConfig.site.shortName}`,
} satisfies TanStackStartSeoSite;

封装应用级 SEO helper

项目里再封装一层,把站点配置和多语言默认值放进去。页面只需要传当前页面的 title、description、canonical 和 locale:

import {
  type GenerateTanStackStartSeoParams,
  generateTanStackStartSeo,
  type TanStackStartSeoAlternates,
} from "@repo/seo";

import { appConfig } from "@/config/app.config";
import { seoSite } from "./seo-site";

type AppSeoOptions = Omit<GenerateTanStackStartSeoParams, "alternates" | "site"> & {
  alternates?: Omit<TanStackStartSeoAlternates, "baseLocale" | "locales">;
};

export function generateAppSeo({ alternates, ...options }: AppSeoOptions) {
  return generateTanStackStartSeo({
    ...options,
    alternates: alternates
      ? {
          ...alternates,
          baseLocale: appConfig.i18n.baseLocale,
          locales: appConfig.i18n.locales,
        }
      : undefined,
    site: seoSite,
  });
}

为页面生成 head

在 TanStack Start route 中使用 head()

import { createFileRoute } from "@tanstack/react-router";

import { generateAppSeo } from "@/shared/lib/seo";

export const Route = createFileRoute("/{-$locale}/(root-layout)/pricing/")({
  head: ({ params }) =>
    generateAppSeo({
      alternates: {
        canonicalPath: "/pricing",
        locale: params.locale,
      },
      description: "选择适合你的 01MVP 会员计划。",
      title: "定价",
    }),
});

generateTanStackStartSeo() 会自动拼接站点名称,并生成 Open Graph 和 Twitter Card 的默认值。

如果一个页面没有传 title,会使用站点默认标题。默认标题应该本身就能读懂,比如 AI 产品实战手册,避免模板拼接后出现 01MVP | 01MVP

动态页面

export const Route = createFileRoute("/{-$locale}/(root-layout)/digital-products/$slug/")({
  loader: async ({ params }) => getProduct(params.slug),
  head: ({ loaderData, params }) =>
    generateAppSeo({
      alternates: {
        canonicalPath: `/digital-products/${loaderData.slug}`,
        locale: params.locale,
      },
      description: loaderData.summary,
      images: loaderData.coverImage
        ? [{ alt: loaderData.title, url: loaderData.coverImage }]
        : undefined,
      openGraphType: "website",
      title: loaderData.title,
    }),
});

Open Graph 和 Twitter Card

generateTanStackStartSeo() 会输出 og:*twitter:* meta。你可以按页面覆盖图片、类型和 robots:

head: () =>
  generateAppSeo({
    alternates: {
      canonicalPath: "/pricing",
    },
    description: "选择适合你的计划",
    images: [
      {
        alt: "产品定价",
        height: 630,
        type: "image/png",
        url: "/images/pricing-og.png",
        width: 1200,
      },
    ],
    openGraphType: "website",
    title: "产品定价",
  }),

页面新增检查

新增页面时,按页面性质处理 SEO:

页面类型要做的事
公开营销页、内容页、产品页写 route head(),传 titledescriptioncanonicalPathlocale
需要被搜索引擎发现的公开页同步更新 products/01mvp/apps/web/src/lib/sitemap.ts 的公开路由列表
Dashboard、Admin、登录、设置、搜索页robots: { index: false, follow: false }
结账、订单、领取、支付结果页robots: { index: false, follow: false },避免索引事务页面
活动页、落地页、旧路径别名优先 redirect;保留页面时,把 canonical 指向主页面,并按需 noindex

JSON-LD 结构化数据

JSON-LD 帮助搜索引擎理解页面内容。TanStack Start 的 head() 支持 script tag,可以直接传给 scripts

import { createJsonLdScript } from "@repo/seo";

head: () =>
  generateAppSeo({
    scripts: [
      createJsonLdScript({
        "@context": "https://schema.org",
        "@type": "WebSite",
        name: "01MVP",
        url: "https://01mvp.com",
      }),
    ],
  });

createJsonLdScript() 会统一做 JSON 序列化和 HTML script 安全转义。

根路由可以放稳定的站点级结构化数据:

head: () => {
  const rootSeo = generateAppSeo({
    includeDocumentMeta: true,
    scripts: [
      createJsonLdScript({
        "@context": "https://schema.org",
        "@type": "WebSite",
        description: appConfig.site.description,
        inLanguage: appConfig.i18n.locales,
        name: appConfig.site.longName,
        publisher: {
          "@type": "Organization",
          name: appConfig.site.publisher,
          url: appConfig.site.baseUrl,
        },
        url: appConfig.site.url,
      }),
    ],
  });

  return {
    links: rootSeo.links,
    meta: rootSeo.meta,
    scripts: rootSeo.scripts,
  };
};

常用 JSON-LD 类型

const organizationSchema = {
  "@context": "https://schema.org",
  "@type": "Organization",
  name: "01MVP",
  url: "https://01mvp.com",
  logo: "https://01mvp.com/images/logo.png",
  sameAs: ["https://x.com/makerjackie", "https://github.com/makerjackie"],
};

const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: "如何快速启动一个 SaaS 项目",
  author: { "@type": "Person", name: "Your Name" },
  datePublished: "2025-01-15",
  image: "https://01mvp.com/images/post-cover.png",
};

const productSchema = {
  "@context": "https://schema.org",
  "@type": "Product",
  name: "01MVP Pro",
  description: "适用于独立开发者的全栈模板",
  offers: {
    "@type": "Offer",
    price: "99",
    priceCurrency: "USD",
  },
};

Sitemap 集成

TanStack Start 可以直接用 TanStack Router 的 server route 输出 sitemap。本模板在 products/01mvp/apps/web/src/routes/sitemap[.]xml.ts 中实现:

import { createFileRoute } from "@tanstack/react-router";

import { buildSitemapResponse } from "@/server/seo";

export const Route = createFileRoute("/sitemap.xml")({
  preload: false,
  server: {
    handlers: {
      GET: () => buildSitemapResponse(),
    },
  },
});

products/01mvp/apps/web/src/server/seo.ts 负责读取站点配置、加载文档源并返回 Response。createSitemapEntries() 负责合并公开页面和文档页,serializeSitemap() 负责输出 XML。新增公开索引页面时,更新 products/01mvp/apps/web/src/lib/sitemap.ts 中的公开路由列表。

robots.txt

同样用 server route 输出 robots.txt,文件位置是 products/01mvp/apps/web/src/routes/robots[.]txt.ts

import { createFileRoute } from "@tanstack/react-router";

import { buildRobotsTxtResponse } from "@/server/seo";

export const Route = createFileRoute("/robots.txt")({
  server: {
    handlers: {
      GET: () => buildRobotsTxtResponse(),
    },
  },
});

注意事项

这个包是可选能力。如果项目只需要基本的 SEO,TanStack Start route head() 已经够用。当多个页面或多个 app 需要共享 SEO 默认值时,再接入 @repo/seo

相关资源

这篇文档有问题?