404 Not Found

404 Not Found


nginx

SEO & Metadata API

SEO is a free traffic engine for websites—correct metadata can make Google index your site three times faster.

1. What You'll Learn


2. A True Story of a Content Operations Manager

(1) Pain Point: Three months after launch, Google has only indexed five pages

Bob is in charge of content operations for TaskFlow. Three months after the blog launched, he had written 50 articles:

"Google Search Console shows that only 5 pages have been indexed. Upon inspection, I found that all pages <title> have 'TaskFlow' as their title, no meta description, and no Open Graph image—so when shared on social media, they appear as plain links."

Issue Impact Quantification
Duplicate title Search engines cannot distinguish between pages Only 5 out of 50 indexed
No OG image No preview image for social sharing Share click-through rate: 0.3%
No JSON-LD No rich media summary No additional information in search results
No Sitemap Google Can't Crawl Deep Pages Indexing Delay of 30 Days

(2) The Next.js Metadata API Solution

Use generateMetadata() to generate unique SEO tags and JSON-LD structured data for each page.

TS
// app/blog/[slug]/page.tsx — News SEO
import type { Metadata } from 'next'

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  return {
    title: `${post.title} - TaskFlow Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.ogImage, width: 1200, height: 630 }]
    }
  }
}

(3) Revenue

Metric Before Optimization After Optimization Improvement
Google Indexing Rate 10% 98% 9.8x
Social Sharing Click-Through Rate 0.3% 2.8% 9.3x
Rich Media Search Results ❌ None ✅ Breadcrumbs + Article Cards
Page Discovery Speed 30 days < 24h 30x

3. Static and Dynamic Metadata

(1) Static Export

TS
// app/about/page.tsx — Static Page metadata
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About TaskFlow - Team Collaboration Platform',
  description: 'TaskFlow Help 10,000+ Highly Effective Team Collaboration,Provide Project Management、Real-time collaboration and intelligent analytics features。',
  keywords: ['Teamwork', 'Project Management', 'TaskFlow', 'SaaS'],
  authors: [{ name: 'TaskFlow Team', url: 'https://taskflow.io' }]
}

(2) Dynamic Generation

TS
// app/products/[id]/page.tsx — News metadata
import type { Metadata, ResolvingMetadata } from 'next'

type Props = { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> }

export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
  const { id } = await params
  const product = await fetch(`https://api.taskflow.io/products/${id}`).then(r => r.json())

  return {
    title: `${product.name} - TaskFlow Products`,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      url: `https://taskflow.io/products/${id}`,
      siteName: 'TaskFlow',
      images: [
        {
          url: product.ogImage,
          width: 1200,
          height: 630,
          alt: product.name
        }
      ],
      locale: 'zh_CN',
      type: 'website'
    },
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      description: product.description,
      images: [product.ogImage]
    },
    alternates: {
      canonical: `https://taskflow.io/products/${id}`,
      languages: {
        'en': `https://taskflow.io/en/products/${id}`,
        'ja': `https://taskflow.io/ja/products/${id}`,
        'ar': `https://taskflow.io/ar/products/${id}`
      }
    }
  }
}

(3) Metadata Field Quick Reference Table

Field Purpose Example
title Page Title / <title> 'Product Details - TaskFlow'
description Search Engine Summary 'TaskFlow Project Management Tool...'
openGraph Facebook / LinkedIn Sharing { title, description, images }
twitter X (Twitter) card { card: 'summary_large_image' }
alternates.canonical Authoritative URL to Prevent Duplicate Content https://taskflow.io/page
alternates.languages hreflang multilingual { 'en': '...' }
robots Reptile command { index: true, follow: true }

▶ Example: Complete SEO for a Blog Post

Output:

TEXT
Async function executes and returns fetched data.
TS
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

interface Post {
  title: string
  excerpt: string
  ogImage: string
  publishedAt: string
  author: string
  tags: string[]
}

async function getPost(slug: string): Promise<Post> {
  const res = await fetch(`https://api.taskflow.io/blog/${slug}`, { next: { revalidate: 3600 } })
  return res.json()
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  return {
    title: `${post.title} | TaskFlow Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
      images: [{ url: post.ogImage, width: 1200, height: 630 }]
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage]
    },
    keywords: [...post.tags, 'TaskFlow', 'Project Management'],
    alternates: { canonical: `https://taskflow.io/blog/${slug}` },
    robots: { index: true, follow: true }
  }
}

Output:

TEXT
Defines TypeScript type(s): Post.

▶ Example: Using the robots meta tag to control web crawlers

Output:

TEXT
Defines TypeScript type(s): Post.
TS
// app/private/dashboard/page.tsx — Prevent search engines from indexing
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Dashboard - TaskFlow',
  robots: {
    index: false,        // Do not index
    follow: false,       // Do Not Track Links
    noarchive: true,     // Do not cache
    nosnippet: true,     // Do not display summary
    nocache: true        // Do not cache
  }
}

export default function DashboardPage() {
  return <h1>Private Dashboard</h1>
}
💻 Output (HTML head):

HTML
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />

Output:

TEXT
Browser renders: "Private Dashboard" heading. Search engines will not index this page — the <meta name="robots" content="noindex, nofollow, noarchive, nosnippet"> tag prevents crawling.

4. JSON-LD Structured Data

(1) Three Common Schemas

100%
graph TB
    A[JSON-LD Structured Data] --> B[BreadcrumbList<br/>Breadcrumb Navigation]
    A --> C[Article<br/>Article Details]
    A --> D[FAQPage<br/>Frequently Asked Questions]

    B --> E[Display the search results path]
    C --> F[Knowledge Panel + Cover Image]
    D --> G[Search results display Q&A directly]

    style A fill:#cce5ff
    style B fill:#d4edda
    style C fill:#d4edda
    style D fill:#d4edda
Schema Use Case Search Results Display
BreadcrumbList All Pages Display breadcrumb trail (Home > Products > Details)
Article Blog Post Article Card (Title + Summary + Cover Image + Publication Date)
FAQ Page Help Center / Frequently Asked Questions Display a list of questions and answers (collapsible)
Product Product Page Price + Inventory + Star Rating

(2) JSON-LD Injection Function

TS
// lib/jsonld.ts — JSON-LD Generation Tools
export function breadcrumbJsonld(items: { name: string; url: string }[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: item.name,
      item: item.url
    }))
  }
}

export function articleJsonld(post: {
  title: string
  excerpt: string
  url: string
  ogImage: string
  publishedAt: string
  author: string
}) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    image: post.ogImage,
    datePublished: post.publishedAt,
    author: { '@type': 'Person', name: post.author },
    publisher: { '@type': 'Organization', name: 'TaskFlow', logo: 'https://taskflow.io/logo.png' },
    mainEntityOfPage: { '@type': 'WebPage', '@id': post.url }
  }
}

▶ Example: Article Page JSON-LD + Breadcrumb

Output:

TEXT
TypeScript module executes successfully.
TSX
// app/blog/[slug]/page.tsx — Complete SEO + JSON-LD
import { breadcrumbJsonld, articleJsonld } from '@/lib/jsonld'

export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  const url = `https://taskflow.io/blog/${slug}`

  const breadcrumb = breadcrumbJsonld([
    { name: 'Home', url: 'https://taskflow.io' },
    { name: 'Blog', url: 'https://taskflow.io/blog' },
    { name: post.title, url }
  ])

  const article = articleJsonld({
    title: post.title,
    excerpt: post.excerpt,
    url,
    ogImage: post.ogImage,
    publishedAt: post.publishedAt,
    author: post.author
  })

  return (
    <>
      {/* JSON-LD Inject */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumb) }}
      />
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(article) }}
      />

      <article>
        <h1>{post.title}</h1>
        <p>{post.excerpt}</p>
        <div>{post.content}</div>
      </article>
    </>
  )
}

Output:

TEXT
Renders the ▶ Example: Article Page JSON-LD + Breadcrumb component UI as described in the section.
💡 Tip: Google Search Console offers a "Rich Results Test" tool that can verify whether JSON-LD is parsed correctly.


5. Sitemap and robots.txt

(1) Dynamic Sitemap Generation

TS
// app/sitemap.ts — News Sitemap(Automatically include all pages)
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://taskflow.io'

  // Static Page
  const staticPages = [
    { url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 1.0 },
    { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
    { url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 0.9 },
    { url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
    { url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'yearly' as const, priority: 0.5 }
  ]

  // Dynamic Blog Posts (from API)
  const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())

  const blogPages = posts.map((post: { slug: string; updatedAt: string }) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7
  }))

  // Multilingual Pages
  const locales = ['en', 'ja', 'ar']
  const localizedPages = locales.flatMap((locale) =>
    staticPages.map((page) => ({
      url: `${baseUrl}/${locale}${page.url.replace(baseUrl, '')}`,
      lastModified: page.lastModified,
      changeFrequency: page.changeFrequency,
      priority: page.priority * 0.9,
      alternates: {
        languages: Object.fromEntries(
          locales.map((l) => [l, `${baseUrl}/${l}${page.url.replace(baseUrl, '')}`])
        )
      }
    }))
  )

  return [...staticPages, ...blogPages, ...localizedPages]
}

(2) robots.txt

TS
// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/_next/', '/dashboard']
      },
      {
        userAgent: 'GPTBot',
        disallow: '/'
      }
    ],
    sitemap: 'https://taskflow.io/sitemap.xml'
  }
}

▶ Example: Multilingual Sitemap Validation

TEXT
# Generated sitemap.xml Excerpt
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <url>
    <loc>https://taskflow.io/about</loc>
    <lastmod>2026-07-06</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
    <xhtml:link rel="alternate" hreflang="en" href="https://taskflow.io/en/about"/>
    <xhtml:link rel="alternate" hreflang="ja" href="https://taskflow.io/ja/about"/>
    <xhtml:link rel="alternate" hreflang="ar" href="https://taskflow.io/ar/about"/>
  </url>
</urlset>

6. Generating Dynamic OG Images

(1) @vercel/og Architecture

100%
graph LR
    A[Share the link on social media] --> B[Web crawler requests OG Image]
    B --> C[@vercel/og Edge Function]
    C --> D[Satori + React]
    D --> E[Render JSX as PNG]
    E --> F[1200×630 OG Image]
    F --> G[Facebook / X / LinkedIn Show]

    style C fill:#cce5ff
    style D fill:#d4edda
    style E fill:#fff3cd
Library Function Description
@vercel/og OG Image Generation Edge Function Zero Server Overhead
Satori JSX → SVG Conversion Less than 1 ms
resvg-wasm SVG → PNG render ~5ms

(2) Installation and Basic Examples

BASH
npm install @vercel/og
TSX
// app/og/route.tsx — OG Image Generation API
import { ImageResponse } from '@vercel/og'

export const runtime = 'edge'

export async function GET() {
  return new ImageResponse(
    (
      <div style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
        color: 'white',
        fontSize: 60,
        fontWeight: 700,
        padding: 40
      }}>
        <h1>TaskFlow</h1>
        <p style={{ fontSize: 32, opacity: 0.9 }}>Team Collaboration Platform</p>
      </div>
    ),
    { width: 1200, height: 630 }
  )
}

▶ Example: Dynamic Article OG Image

Output:

TEXT
Renders the GET component UI.
TSX
// app/blog/[slug]/og/route.tsx — Featured Articles OG Image
import { ImageResponse } from '@vercel/og'

export const runtime = 'edge'

export async function GET(req: Request, { params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params

  // Get Article Information
  const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())

  return new ImageResponse(
    (
      <div style={{
        width: 1200,
        height: 630,
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        padding: 60,
        background: 'linear-gradient(135deg, #1e1b4b 0%, #4f46e5 100%)',
        color: 'white'
      }}>
        {/* Tags */}
        <div style={{ display: 'flex', gap: 8 }}>
          {post.tags?.slice(0, 3).map((tag: string) => (
            <span key={tag} style={{
              padding: '4px 12px',
              borderRadius: 20,
              background: 'rgba(255,255,255,0.2)',
              fontSize: 18
            }}>{tag}</span>
          ))}
        </div>

        {/* Title */}
        <h1 style={{ fontSize: 52, margin: '20px 0', lineHeight: 1.2 }}>
          {post.title}
        </h1>

        {/* Author + Date */}
        <div style={{ display: 'flex', gap: 16, fontSize: 22, opacity: 0.8 }}>
          <span>{post.author}</span>
          <span>{new Date(post.publishedAt).toLocaleDateString('zh-CN')}</span>
        </div>

        {/* Logo */}
        <div style={{ position: 'absolute', bottom: 40, right: 60, fontSize: 28, fontWeight: 'bold' }}>
          TaskFlow Blog
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  )
}

Output:

TEXT
Fetches data server-side and renders a list of items from post.
Visible content: ))} | TaskFlow Blog
💡 Tip: The OG image route handler is at /blog/[slug]/og/route.tsx, and the final link is https://taskflow.io/blog/nextjs-seo-guide/og. Simply reference this address in generateMetadata.


7. Complete Example: Comprehensive Implementation of Multilingual SEO + OG Images

TSX
// app/layout.tsx — Root Layout (Default) SEO + Multilingual
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: { template: '%s | TaskFlow', default: 'TaskFlow - Team Collaboration Platform' },
  description: 'TaskFlow Helping the World 10,000+ Highly Effective Team Collaboration。Provide Project Management、Real-Time Collaboration、AI Intelligent Analysis。',
  openGraph: {
    siteName: 'TaskFlow',
    type: 'website',
    locale: 'zh_CN',
    images: [{ url: 'https://taskflow.io/og-default.png', width: 1200, height: 630 }]
  },
  twitter: { card: 'summary_large_image', site: '@taskflow' },
  robots: { index: true, follow: true },
  alternates: {
    canonical: 'https://taskflow.io',
    languages: {
      'en': 'https://taskflow.io/en',
      'ja': 'https://taskflow.io/ja',
      'ar': 'https://taskflow.io/ar'
    }
  }
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>{children}</body>
    </html>
  )
}
TS
// app/sitemap.ts — Complete Sitemap
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://taskflow.io'
  const locales = ['zh', 'en', 'ja', 'ar'] as const
  
  const pages = ['', '/about', '/blog', '/pricing', '/contact']
  
  return pages.flatMap((page) =>
    locales.map((locale) => ({
      url: `${baseUrl}/${locale}${page}`,
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: page === '' ? 1.0 : 0.8,
      alternates: {
        languages: Object.fromEntries(locales.map((l) => [l, `${baseUrl}/${l}${page}`]))
      }
    }))
  )
}
TS
// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/dashboard'] },
    sitemap: 'https://taskflow.io/sitemap.xml'
  }
}
TSX
// app/blog/[slug]/page.tsx — The article page consolidates all SEO
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 fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
      images: [{ url: `https://taskflow.io/blog/${slug}/og`, width: 1200, height: 630 }]
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [`https://taskflow.io/blog/${slug}/og`]
    },
    alternates: {
      canonical: `https://taskflow.io/blog/${slug}`,
      languages: {
        en: `https://taskflow.io/en/blog/${slug}`,
        ja: `https://taskflow.io/ja/blog/${slug}`,
        ar: `https://taskflow.io/ar/blog/${slug}`
      }
    }
  }
}

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params

  return (
    <article>
      {/* JSON-LD Structured Data */}
      <script type="application/ld+json" dangerouslySetInnerHTML={{
        __html: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'BreadcrumbList',
          itemListElement: [
            { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://taskflow.io' },
            { '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://taskflow.io/blog' },
            { '@type': 'ListItem', position: 3, name: slug }
          ]
        })
      }} />

      <h1>Article Title</h1>
      <div>Article Content...</div>
    </article>
  )
}

❓ FAQ

Q What is the difference between the exports from generateMetadata() and metadata?
A metadata is a static export used for static routing (such as /about). generateMetadata() is an asynchronous function that dynamically generates metadata based on params / searchParams and is used for dynamic routing (such as /blog/[slug]). The two cannot be used simultaneously.
Q What is the relationship between JSON-LD and meta tags?
A Meta tags (<title> / <meta name="description">) are basic SEO tags. JSON-LD (<script type="application/ld+json">) is structured data that enables Google to display rich snippets (breadcrumbs, star ratings, collapsible FAQs). The two are complementary and both need to be implemented.
Q Does generating dynamic OG images increase the server load?
A @vercel/og runs on Edge Runtime (edge computing), and each image takes only ~5 ms to generate, resulting in virtually zero overhead. In a production environment, we recommend enabling CDN caching (Cache-Control: public, max-age=31536000, immutable).
Q Where should the hreflang tag be configured?
A Configure it in generateMetadata() at alternates.languages. Next.js automatically injects <link rel="alternate" hreflang="en" href="..."> into the page <head>. You also need to include the corresponding xhtml:link tag in the sitemap.
Q Do the changeFrequency and priority fields in the sitemap affect Google rankings?
A Google has officially stated that it ignores these two fields. They still have reference value for search engines such as Bing and Yandex. We recommend that your sitemap include all pages and that the lastModified field be accurate—this is important for crawlers to determine whether to recrawl the pages.

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Configure the metadata export in the static route /about, including the title, description, Open Graph, and Twitter card.

  2. Advanced Exercise (⭐⭐): Implement full SEO for a news detail page: generateMetadata() Dynamically generate the title, description, OG tags, and Twitter card, inject Article JSON-LD structured data, and list all article URLs in the sitemap.

  3. Challenge (⭐⭐⭐): Build a complete OG image generation system: @vercel/og Generate a 1200×630 share image for each blog post, including the title, author, and tags; reference the image URL in generateMetadata(); and finally, use the Google Rich Results Test to verify the correctness of the JSON-LD and OG tags.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏