Remix SEO优化 #

一、SEO概述 #

Remix 提供了强大的 SEO 支持,包括 meta 函数、links 函数和服务端渲染。

二、Meta标签 #

2.1 基本用法 #

tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = () => {
  return [
    { title: "我的网站 - 首页" },
    { name: "description", content: "这是我的网站描述" },
    { name: "keywords", content: "关键词1, 关键词2" },
  ];
};

2.2 动态Meta #

tsx
import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  return json({ post });
}

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: `${data?.post.title} - 我的博客` },
    { name: "description", content: data?.post.excerpt },
    { name: "keywords", content: data?.post.tags?.join(", ") },
  ];
};

2.3 Open Graph #

tsx
export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title },
    { property: "og:title", content: data?.post.title },
    { property: "og:description", content: data?.post.excerpt },
    { property: "og:image", content: data?.post.coverImage },
    { property: "og:type", content: "article" },
    { property: "og:url", content: `https://example.com/posts/${data?.post.id}` },
  ];
};

2.4 Twitter Card #

tsx
export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { name: "twitter:card", content: "summary_large_image" },
    { name: "twitter:title", content: data?.post.title },
    { name: "twitter:description", content: data?.post.excerpt },
    { name: "twitter:image", content: data?.post.coverImage },
  ];
};

三、结构化数据 #

3.1 JSON-LD #

tsx
export default function Post() {
  const { post } = useLoaderData<typeof loader>();
  
  const structuredData = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    author: {
      "@type": "Person",
      name: post.author.name,
    },
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
  };
  
  return (
    <article>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
      />
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

3.2 面包屑结构化数据 #

tsx
export default function Product() {
  const { product, category } = useLoaderData<typeof loader>();
  
  const breadcrumbData = {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: [
      {
        "@type": "ListItem",
        position: 1,
        name: "首页",
        item: "https://example.com",
      },
      {
        "@type": "ListItem",
        position: 2,
        name: category.name,
        item: `https://example.com/categories/${category.slug}`,
      },
      {
        "@type": "ListItem",
        position: 3,
        name: product.name,
      },
    ],
  };
  
  return (
    <div>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbData) }}
      />
      {/* 页面内容 */}
    </div>
  );
}

四、站点地图 #

4.1 动态站点地图 #

创建 app/routes/sitemap[.]xml.ts

tsx
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({}: LoaderFunctionArgs) {
  const posts = await getPosts();
  const pages = await getPages();
  
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com</loc>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  ${posts.map((post) => `
    <url>
      <loc>https://example.com/posts/${post.slug}</loc>
      <lastmod>${post.updatedAt}</lastmod>
      <changefreq>weekly</changefreq>
      <priority>0.8</priority>
    </url>
  `).join("")}
  ${pages.map((page) => `
    <url>
      <loc>https://example.com/${page.slug}</loc>
      <lastmod>${page.updatedAt}</lastmod>
      <changefreq>monthly</changefreq>
      <priority>0.6</priority>
    </url>
  `).join("")}
</urlset>`;
  
  return new Response(sitemap, {
    headers: {
      "Content-Type": "application/xml",
    },
  });
}

4.2 robots.txt #

创建 app/routes/robots[.]txt.ts

tsx
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({}: LoaderFunctionArgs) {
  const robots = `User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml`;

  return new Response(robots, {
    headers: {
      "Content-Type": "text/plain",
    },
  });
}

五、规范链接 #

5.1 Canonical URL #

tsx
export const links: LinksFunction = () => [
  { rel: "canonical", href: "https://example.com/posts/my-post" },
];

5.2 多语言链接 #

tsx
export const links: LinksFunction = () => [
  { rel: "alternate", hrefLang: "en", href: "https://example.com/en/post" },
  { rel: "alternate", hrefLang: "zh", href: "https://example.com/zh/post" },
  { rel: "alternate", hrefLang: "x-default", href: "https://example.com/post" },
];

六、性能与SEO #

6.1 预渲染关键页面 #

tsx
export const headers = () => ({
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
});

6.2 图片优化 #

tsx
export const links: LinksFunction = () => [
  {
    rel: "preload",
    href: "/images/hero.webp",
    as: "image",
  },
];

七、最佳实践 #

7.1 SEO检查清单 #

  • [ ] 每个页面有唯一的title
  • [ ] 每个页面有描述性的meta description
  • [ ] 使用语义化HTML标签
  • [ ] 图片有alt属性
  • [ ] 链接可爬取
  • [ ] 有站点地图
  • [ ] 有robots.txt
  • [ ] 使用HTTPS
  • [ ] 页面加载速度快

7.2 常见问题 #

  • 重复内容:使用canonical标签
  • 动态内容:确保服务端渲染
  • 图片SEO:使用alt属性和懒加载

八、总结 #

本章我们学习了:

  1. Meta标签:动态设置页面元数据
  2. 结构化数据:JSON-LD格式
  3. 站点地图:动态生成sitemap.xml
  4. 规范链接:canonical和多语言链接
  5. 最佳实践:SEO检查清单

核心要点:

  • 使用meta函数设置页面元数据
  • 添加结构化数据增强搜索结果
  • 生成站点地图帮助爬虫
  • 使用语义化HTML
最后更新:2026-03-28