You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于统一配置为Next.js App Router和Remix v2生成SSR元标签?

在Monorepo中统一Next.js App Router与Remix v2的SEO元标签管理

1. 构建共享SEO工具包(Monorepo公共包)

在monorepo中新建公共包(比如@repo/seo),统一SEO元数据的结构、生成逻辑,彻底消除重复实现。

定义统一的SEO类型

创建packages/seo/src/types.ts:

export interface SeoMetadata {
  title: string;
  description: string;
  canonicalUrl?: string;
  robots: {
    index: boolean;
    follow: boolean;
    maxSnippet?: number;
    maxImagePreview?: 'none' | 'standard' | 'large';
    maxVideoPreview?: number;
  };
  openGraph: {
    title?: string;
    description?: string;
    images: Array<{
      url: string;
      width?: number;
      height?: number;
      alt?: string;
    }>;
    url?: string;
  };
  twitterCard: {
    cardType: 'summary' | 'summary_large_image' | 'app' | 'player';
    title?: string;
    description?: string;
    image?: string;
    imageAlt?: string;
  };
}

export interface PostSeoData {
  title: string;
  excerpt: string;
  coverImage: string;
  slug: string;
  canonicalUrl?: string;
}

编写统一的SEO生成函数

创建packages/seo/src/generate.ts:

import { SeoMetadata, PostSeoData } from './types';

export function generatePostSeoMetadata(data: PostSeoData): SeoMetadata {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.REMIX_PUBLIC_APP_URL;
  const canonicalUrl = data.canonicalUrl || `${baseUrl}/blog/${data.slug}`;

  return {
    title: data.title,
    description: data.excerpt,
    canonicalUrl,
    robots: {
      index: true,
      follow: true,
      maxSnippet: 200,
      maxImagePreview: 'large',
    },
    openGraph: {
      title: data.title,
      description: data.excerpt,
      images: [
        {
          url: data.coverImage,
          width: 1200,
          height: 630,
          alt: data.title,
        },
      ],
      url: canonicalUrl,
    },
    twitterCard: {
      cardType: 'summary_large_image',
      title: data.title,
      description: data.excerpt,
      image: data.coverImage,
      imageAlt: data.title,
    },
  };
}

2. Next.js App Router 适配

在Next.js应用中导入共享工具包,将统一格式的SEO数据转换为框架要求的结构:

// app/blog/[slug]/page.tsx
import { generatePostSeoMetadata } from '@repo/seo';
import getPost from '@/lib/getPost';

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const seo = generatePostSeoMetadata(post);

  return {
    title: seo.title,
    description: seo.description,
    alternates: {
      canonical: seo.canonicalUrl,
    },
    robots: {
      index: seo.robots.index,
      follow: seo.robots.follow,
      maxSnippet: seo.robots.maxSnippet,
      maxImagePreview: seo.robots.maxImagePreview,
      maxVideoPreview: seo.robots.maxVideoPreview,
    },
    openGraph: {
      title: seo.openGraph.title,
      description: seo.openGraph.description,
      images: seo.openGraph.images,
      url: seo.openGraph.url,
    },
    twitter: {
      card: seo.twitterCard.cardType,
      title: seo.twitterCard.title,
      description: seo.twitterCard.description,
      images: [
        {
          url: seo.twitterCard.image,
          alt: seo.twitterCard.imageAlt,
        },
      ],
    },
  };
}

3. Remix v2 适配

在Remix应用中同样导入共享工具包,将统一格式转换为框架要求的meta数组:

// app/routes/blog.$slug.tsx
import { generatePostSeoMetadata } from '@repo/seo';
import { MetaFunction } from '@remix-run/node';

export const meta: MetaFunction = ({ data }) => {
  const seo = generatePostSeoMetadata(data.post);
  const metaTags = [];

  // 基础标签
  metaTags.push({ title: seo.title });
  metaTags.push({ name: 'description', content: seo.description });
  if (seo.canonicalUrl) {
    metaTags.push({ rel: 'canonical', href: seo.canonicalUrl });
  }

  // Robots指令拼接
  const robotsParts = [];
  robotsParts.push(seo.robots.index ? 'index' : 'noindex');
  robotsParts.push(seo.robots.follow ? 'follow' : 'nofollow');
  if (seo.robots.maxSnippet) robotsParts.push(`max-snippet:${seo.robots.maxSnippet}`);
  if (seo.robots.maxImagePreview) robotsParts.push(`max-image-preview:${seo.robots.maxImagePreview}`);
  if (seo.robots.maxVideoPreview) robotsParts.push(`max-video-preview:${seo.robots.maxVideoPreview}`);
  metaTags.push({ name: 'robots', content: robotsParts.join(', ') });

  // Open Graph标签
  metaTags.push({ property: 'og:title', content: seo.openGraph.title || seo.title });
  metaTags.push({ property: 'og:description', content: seo.openGraph.description || seo.description });
  metaTags.push({ property: 'og:url', content: seo.openGraph.url || seo.canonicalUrl });
  seo.openGraph.images.forEach((img, idx) => {
    metaTags.push({ property: idx === 0 ? 'og:image' : `og:image:${idx+1}`, content: img.url });
    if (img.width) metaTags.push({ property: idx === 0 ? 'og:image:width' : `og:image:${idx+1}:width`, content: img.width.toString() });
    if (img.height) metaTags.push({ property: idx === 0 ? 'og:image:height' : `og:image:${idx+1}:height`, content: img.height.toString() });
    if (img.alt) metaTags.push({ property: idx === 0 ? 'og:image:alt' : `og:image:${idx+1}:alt`, content: img.alt });
  });

  // Twitter Card标签
  metaTags.push({ name: 'twitter:card', content: seo.twitterCard.cardType });
  metaTags.push({ name: 'twitter:title', content: seo.twitterCard.title || seo.title });
  metaTags.push({ name: 'twitter:description', content: seo.twitterCard.description || seo.description });
  if (seo.twitterCard.image) {
    metaTags.push({ name: 'twitter:image', content: seo.twitterCard.image });
    if (seo.twitterCard.imageAlt) metaTags.push({ name: 'twitter:image:alt', content: seo.twitterCard.imageAlt });
  }

  return metaTags;
};

4. 维护与扩展

后续调整SEO规则(比如修改robots默认值、新增OG字段),只需修改@repo/seo中的生成逻辑,两个应用会自动同步更新,无需分别修改两套代码。

内容的提问来源于stack exchange,提问作者Al Amin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 16:37:27