mcpField
S
seo-technical

seo-technical

github.com/andrehfpマーケティング・グロース7437 回インストール40 週間前に更新
リポジトリ

seo-technical について

Next.js App Router を使う開発者向けの技術 SEO 実装ガイドです。サイトマップ、クローラー設定、メタデータ、構造化データ、公開前の確認項目を扱います。

主な機能

  1. 01動的サイトマップの生成
  2. 02robots.txt のクローラー設定
  3. 03ページメタデータとソーシャルカードの設定
  4. 04JSON-LD 構造化データの追加
  5. 05正規 URL と SEO チェックリストの管理

活用シーン

  1. 01動的ページのサイトマップ生成
  2. 02環境に応じたクローラー規則の設定
  3. 03検索・ソーシャル向けページ情報の整備
  4. 04公開前の技術 SEO 設定確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/andrehfp/tinyplate --skill seo-technical

インストール後、Claude Code で/seo-technicalを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
checklist.md
nextjs-implementation.md
structured-data.md

詳細

テクニカル SEO の実装(Next.js 2025)

スキルファイル

このスキルには、複数のリファレンスファイルが含まれています。

  • SKILL.md(このファイル):テクニカル SEO の中核となる実装ガイド
  • nextjs-implementation.md:Next.js 固有のコードテンプレートとパターン
  • checklist.md:公開前のテクニカル SEO チェックリスト
  • structured-data.md:JSON-LD 構造化データのテンプレート

このスキルで扱う内容

  1. サイトマップ → 動的なサイトマップ生成用の app/sitemap.ts
  2. Robots.txt → クローラー向けディレクティブ用の app/robots.ts
  3. メタタグ → OpenGraph、Twitter Cards、キーワード、説明
  4. 構造化データ → リッチスニペット用の JSON-LD
  5. 正規 URL → 重複コンテンツの問題を防止
  6. パフォーマンス SEO → Core Web Vitals に関する考慮事項

パート 1:サイトマップの実装

Next.js App Router のサイトマップ(app/sitemap.ts)

app/sitemap.ts を作成すると、Next.js は /sitemap.xml を自動的に提供します。

import type { MetadataRoute } from "next";

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";

export default function sitemap(): MetadataRoute.Sitemap {
  const currentDate = new Date().toISOString();

  // Static pages
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: BASE_URL,
      lastModified: currentDate,
      changeFrequency: "weekly",
      priority: 1.0,
    },
    {
      url: `${BASE_URL}/pricing`,
      lastModified: currentDate,
      changeFrequency: "monthly",
      priority: 0.8,
    },
    {
      url: `${BASE_URL}/about`,
      lastModified: currentDate,
      changeFrequency: "monthly",
      priority: 0.7,
    },
    {
      url: `${BASE_URL}/privacy`,
      lastModified: currentDate,
      changeFrequency: "yearly",
      priority: 0.3,
    },
    {
      url: `${BASE_URL}/terms`,
      lastModified: currentDate,
      changeFrequency: "yearly",
      priority: 0.3,
    },
  ];

  return staticPages;
}
データベースのコンテンツを含む動的サイトマップ
import type { MetadataRoute } from "next";
import { db } from "@/lib/db";
import { blogPosts, products } from "@/lib/db/schema";

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  // Fetch dynamic content
  const posts = await db.select().from(blogPosts).where(eq(blogPosts.published, true));
  const allProducts = await db.select().from(products);

  const staticPages: MetadataRoute.Sitemap = [
    { url: BASE_URL, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
  ];

  const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${BASE_URL}/blog/${post.slug}`,
    lastModified: post.updatedAt || post.createdAt,
    changeFrequency: "weekly" as const,
    priority: 0.7,
  }));

  const productPages: MetadataRoute.Sitemap = allProducts.map((product) => ({
    url: `${BASE_URL}/products/${product.slug}`,
    lastModified: product.updatedAt,
    changeFrequency: "daily" as const,
    priority: 0.8,
  }));

  return [...staticPages, ...blogPages, ...productPages];
}
大規模なサイトマップ(URL 50,000 件以上)

サイトマップインデックスには generateSitemaps() を使用します。

import type { MetadataRoute } from "next";

const URLS_PER_SITEMAP = 50000;

export async function generateSitemaps() {
  const totalProducts = await getProductCount();
  const sitemapCount = Math.ceil(totalProducts / URLS_PER_SITEMAP);

  return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }));
}

export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
  const start = id * URLS_PER_SITEMAP;
  const products = await getProducts({ start, limit: URLS_PER_SITEMAP });

  return products.map((product) => ({
    url: `${BASE_URL}/products/${product.slug}`,
    lastModified: product.updatedAt,
  }));
}
サイトマップのベストプラクティス
プラクティス理由
lastModified を正確に保つ一貫して正確であれば、Google はこれを使用します
正規 URL のみを含める重複はクロールバジェットを浪費します
優先度:ホームページは 1.0、主要ページは 0.8-0.9、その他は 0.6-0.7クローラーにページの重要度を伝えます
Google は changeFrequency を無視しますほかの検索エンジン向けに含めてください
サイトマップあたり最大 50,000 件の URLそれ以上の場合はサイトマップインデックスを使用します

パート 2:Robots.txt の実装

Next.js App Router の Robots(app/robots.ts)

import type { MetadataRoute } from "next";

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";

export default function robots(): MetadataRoute.Robots {
  const isProduction = process.env.NODE_ENV === "production";

  // Block everything in non-production
  if (!isProduction) {
    return {
      rules: { userAgent: "*", disallow: "/" },
    };
  }

  return {
    rules: [
      {
        userAgent: "*",
        allow: "/",
        disallow: [
          "/api/",
          "/dashboard/",
          "/admin/",
          "/private/",
          "/_next/",
          "/sign-in/",
          "/sign-up/",
        ],
      },
      // Block AI training bots (optional)
      { userAgent: "GPTBot", disallow: "/" },
      { userAgent: "ChatGPT-User", disallow: "/" },
      { userAgent: "CCBot", disallow: "/" },
      { userAgent: "anthropic-ai", disallow: "/" },
      { userAgent: "Google-Extended", disallow: "/" },
    ],
    sitemap: `${BASE_URL}/sitemap.xml`,
  };
}
Robots.txt のルール
ディレクティブ用途
User-agent: *すべてのクローラーに適用されます
Allow: /パスのクロールを許可します
Disallow: /private/パスのクロールをブロックします
Sitemap:サイトマップの場所を通知します
Crawl-delay:クロールを遅くします(Google はこの指示を尊重しません)
ブロック/許可する主な AI ボット
// Block AI training (keeps content out of training data)
{ userAgent: "GPTBot", disallow: "/" },           // OpenAI
{ userAgent: "ChatGPT-User", disallow: "/" },     // ChatGPT browsing
{ userAgent: "CCBot", disallow: "/" },            // Common Crawl
{ userAgent: "anthropic-ai", disallow: "/" },     // Anthropic
{ userAgent: "Google-Extended", disallow: "/" },  // Google AI training
{ userAgent: "Bytespider", disallow: "/" },       // ByteDance

// Allow AI search (keeps content in AI search results)
// Comment out the above to allow AI indexing
ブロックしてはいけないもの
  • /sitemap.xml をブロックしないでください
  • CSS/JS ファイル(/_next/static/)をブロックしないでください
  • インデックス登録する画像をブロックしないでください
  • ホームページをブロックしないでください

パート 3:メタデータの実装

ルートレイアウトのメタデータ(app/layout.tsx)

import type { Metadata, Viewport } from "next";

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  themeColor: "#6366f1",
};

export const metadata: Metadata = {
  metadataBase: new URL(BASE_URL),

  // Title template for child pages
  title: {
    default: "Brand Name — Tagline",
    template: "%s | Brand Name",
  },

  // Description (150-160 chars ideal)
  description: "Your compelling meta description that includes primary keywords and encourages clicks.",

  // Keywords (less important now, but include)
  keywords: ["primary keyword", "secondary keyword", "brand name"],

  // Author info
  authors: [{ name: "Brand Name" }],
  creator: "Brand Name",
  publisher: "Brand Name",

  // Robots directives
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      "max-video-preview": -1,
      "max-image-preview": "large",
      "max-snippet": -1,
    },
  },

  // OpenGraph (Facebook, LinkedIn, etc.)
  openGraph: {
    type: "website",
    locale: "en_US",
    url: BASE_URL,
    siteName: "Brand Name",
    title: "Brand Name — Tagline",
    description: "Your compelling description for social sharing.",
    images: [
      {
        url: "/og-image.png",
        width: 1200,
        height: 630,
        alt: "Brand Name - Description",
      },
    ],
  },

  // Twitter Card
  twitter: {
    card: "summary_large_image",
    title: "Brand Name — Tagline",
    description: "Your compelling description for Twitter.",
    images: ["/og-image.png"],
    creator: "@twitterhandle",
    site: "@twitterhandle",
  },

  // Canonical URL
  alternates: {
    canonical: BASE_URL,
  },

  // App categorization
  category: "Technology",

  // Verification codes
  verification: {
    google: "google-site-verification-code",
    // yandex: "yandex-verification-code",
    // bing: "bing-verification-code",
  },
};

ページ単位のメタデータ

// app/pricing/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Pricing", // Becomes "Pricing | Brand Name" via template
  description: "Simple, transparent pricing. Start free, upgrade when you need more.",
  openGraph: {
    title: "Pricing | Brand Name",
    description: "Simple, transparent pricing. Start free, upgrade when you need more.",
  },
};

export default function PricingPage() {
  // ...
}

動的メタデータ(generateMetadata)

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) {
    return { title: "Post Not Found" };
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
    alternates: {
      canonical: `${BASE_URL}/blog/${slug}`,
    },
  };
}

パート 4:認証ミドルウェアとの連携

認証(Clerk、NextAuth など)を使用する場合は、SEO 用ルートを公開マッチャーに追加します。

Clerk(proxy.ts または middleware.ts)

import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isPublicRoute = createRouteMatcher([
  "/",
  "/sign-in(.*)",
  "/sign-up(.*)",
  "/pricing",
  "/about",
  "/blog(.*)",
  "/terms",
  "/privacy",
  // SEO files - IMPORTANT!
  "/robots.txt",
  "/sitemap.xml",
  "/sitemap(.*).xml",
  // Icons
  "/icon(.*)",
  "/apple-icon(.*)",
  "/favicon.ico",
]);

export const proxy = clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

export const config = {
  matcher: [
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    "/(api|trpc)(.*)",
  ],
};

NextAuth

export { auth as middleware } from "@/auth";

export const config = {
  matcher: [
    // Exclude SEO files from auth
    "/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|sitemap.*\\.xml).*)",
  ],
};

パート 5:環境変数

SEO に必要な環境変数:

# .env.local (development)
NEXT_PUBLIC_SITE_URL=http://localhost:3000

# .env.production (production)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com

クイックリファレンス:ファイルの場所

ファイル場所用途
サイトマップapp/sitemap.ts/sitemap.xml を生成します
Robotsapp/robots.ts/robots.txt を生成します
ルートメタデータapp/layout.tsxデフォルトのメタタグ
ページメタデータapp/[route]/page.tsxページ固有のメタデータ
OG 画像public/og-image.pngソーシャル共有用画像(1200x630)
ファビコンapp/icon.tsx または public/favicon.icoブラウザータブのアイコン
Apple アイコンapp/apple-icon.tsx または public/apple-icon.pngiOS アイコン

実装チェックリスト

実装前に確認します。

  1. NEXT_PUBLIC_SITE_URL 環境変数が設定されている
  2. 認証ミドルウェアが /robots.txt と /sitemap.xml を許可している
  3. public/og-image.png に OG 画像がある(1200x630px)
  4. すべての公開ページに固有のタイトルと説明がある
  5. 正規 URL が優先するバージョンを指している

実装後に確認します。

  1. /robots.txt にアクセスし、ルールが表示されることを確認する
  2. /sitemap.xml にアクセスし、URL が表示されることを確認する
  3. Google リッチリザルト テストでテストする
  4. Facebook Sharing Debuggerでテストする
  5. Google Search Console にサイトマップを送信する

コメント