fe-seo
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFE SEO & Metadata Optimization
前端SEO与元数据优化
$ARGUMENTS分析以优化或生成SEO相关元数据。
$ARGUMENTS분석 절차
分析流程
- 현재 상태 파악: 프로젝트의 메타데이터 설정을 Glob/Read로 확인한다
- SEO 체크리스트 검사: 아래 항목에 대해 누락 사항을 확인한다
- 개선안 제시: 구체적인 코드와 함께 최적화 방안을 제시한다
- 구현: 승인 후 메타데이터를 추가/수정한다
- 掌握当前状态:通过Glob/Read查看项目的元数据设置
- 检查SEO检查清单:检查以下项目的遗漏情况
- 提出改进方案:结合具体代码给出优化方案
- 实施:获得批准后添加/修改元数据
Next.js Metadata API
Next.js Metadata API
정적 Metadata
静态Metadata
tsx
// src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "사이트명",
template: "%s | 사이트명", // 하위 페이지에서 title만 지정하면 자동 조합
},
description: "사이트 설명 (155자 이내 권장)",
keywords: ["키워드1", "키워드2", "키워드3"],
authors: [{ name: "작성자명" }],
creator: "회사명",
openGraph: {
type: "website",
locale: "ko_KR",
url: "https://example.com",
siteName: "사이트명",
title: "사이트명",
description: "사이트 설명",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "사이트명 대표 이미지",
},
],
},
twitter: {
card: "summary_large_image",
title: "사이트명",
description: "사이트 설명",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
verification: {
google: "google-verification-code",
naver: "naver-verification-code",
},
};tsx
// src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "站点名称",
template: "%s | 站点名称", // 子页面只需指定title即可自动组合
},
description: "站点描述(建议155字以内)",
keywords: ["关键词1", "关键词2", "关键词3"],
authors: [{ name: "作者名称" }],
creator: "公司名称",
openGraph: {
type: "website",
locale: "ko_KR",
url: "https://example.com",
siteName: "站点名称",
title: "站点名称",
description: "站点描述",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "站点名称代表图片",
},
],
},
twitter: {
card: "summary_large_image",
title: "站点名称",
description: "站点描述",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
verification: {
google: "google-verification-code",
naver: "naver-verification-code",
},
};동적 Metadata (페이지별)
动态Metadata(按页面)
tsx
// src/app/blog/[slug]/page.tsx
import type { Metadata } from "next";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: "포스트를 찾을 수 없습니다" };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
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],
},
};
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
// ...
}tsx
// src/app/blog/[slug]/page.tsx
import type { Metadata } from "next";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: "无法找到文章" };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
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],
},
};
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
// ...
}JSON-LD 구조화 데이터
JSON-LD结构化数据
웹사이트 (조직)
网站(组织)
tsx
// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "회사명",
url: "https://example.com",
logo: "https://example.com/logo.png",
sameAs: [
"https://twitter.com/example",
"https://github.com/example",
],
};
return (
<html lang="ko">
<body>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
</body>
</html>
);
}tsx
// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "公司名称",
url: "https://example.com",
logo: "https://example.com/logo.png",
sameAs: [
"https://twitter.com/example",
"https://github.com/example",
],
};
return (
<html lang="ko">
<body>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
</body>
</html>
);
}블로그 포스트 (Article)
博客文章(Article)
tsx
// src/app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: post.author.name,
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* ... */}</article>
</>
);
}tsx
// src/app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: post.author.name,
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* ... */}</article>
</>
);
}상품 (Product)
商品(Product)
tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.images,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "KRW",
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
aggregateRating: {
"@type": "AggregateRating",
ratingValue: product.rating,
reviewCount: product.reviewCount,
},
};tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.images,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "KRW",
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
aggregateRating: {
"@type": "AggregateRating",
ratingValue: product.rating,
reviewCount: product.reviewCount,
},
};FAQ
FAQ
tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
};tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
};BreadcrumbList
BreadcrumbList(面包屑导航)
tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "홈", item: "https://example.com" },
{ "@type": "ListItem", position: 2, name: "블로그", item: "https://example.com/blog" },
{ "@type": "ListItem", position: 3, name: post.title },
],
};tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "首页", item: "https://example.com" },
{ "@type": "ListItem", position: 2, name: "博客", item: "https://example.com/blog" },
{ "@type": "ListItem", position: 3, name: post.title },
],
};Sitemap
Sitemap(站点地图)
정적 Sitemap
静态Sitemap
typescript
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://example.com",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
{
url: "https://example.com/about",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
];
}typescript
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://example.com",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
{
url: "https://example.com/about",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
];
}동적 Sitemap (DB에서 생성)
动态Sitemap(从数据库生成)
typescript
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await db.post.findMany({
select: { slug: true, updatedAt: true },
});
const postEntries = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "weekly" as const,
priority: 0.7,
}));
return [
{
url: "https://example.com",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
...postEntries,
];
}typescript
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await db.post.findMany({
select: { slug: true, updatedAt: true },
});
const postEntries = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "weekly" as const,
priority: 0.7,
}));
return [
{
url: "https://example.com",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
...postEntries,
];
}대규모 사이트맵 (50,000개 초과)
大规模站点地图(超过50,000条)
typescript
// src/app/sitemap/[id]/route.ts — 여러 사이트맵 파일로 분할
export async function generateSitemaps() {
const totalProducts = await db.product.count();
const numberOfSitemaps = Math.ceil(totalProducts / 50000);
return Array.from({ length: numberOfSitemaps }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const start = id * 50000;
const products = await db.product.findMany({
skip: start,
take: 50000,
select: { slug: true, updatedAt: true },
});
return products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: product.updatedAt,
}));
}typescript
// src/app/sitemap/[id]/route.ts — 拆分为多个站点地图文件
export async function generateSitemaps() {
const totalProducts = await db.product.count();
const numberOfSitemaps = Math.ceil(totalProducts / 50000);
return Array.from({ length: numberOfSitemaps }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const start = id * 50000;
const products = await db.product.findMany({
skip: start,
take: 50000,
select: { slug: true, updatedAt: true },
});
return products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: product.updatedAt,
}));
}Robots.txt
Robots.txt
typescript
// src/app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/private/"],
},
],
sitemap: "https://example.com/sitemap.xml",
};
}typescript
// src/app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/private/"],
},
],
sitemap: "https://example.com/sitemap.xml",
};
}동적 OG 이미지 생성
动态OG图片生成
tsx
// src/app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const title = searchParams.get("title") ?? "Default Title";
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
color: "#fafafa",
fontSize: 48,
fontWeight: 700,
}}
>
<div style={{ marginBottom: 24 }}>사이트명</div>
<div style={{ fontSize: 32, color: "#a1a1aa" }}>{title}</div>
</div>
),
{ width: 1200, height: 630 }
);
}tsx
// 페이지에서 동적 OG 이미지 연결
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
openGraph: {
images: [`/api/og?title=${encodeURIComponent(post.title)}`],
},
};
}tsx
// src/app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const title = searchParams.get("title") ?? "Default Title";
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
color: "#fafafa",
fontSize: 48,
fontWeight: 700,
}}
>
<div style={{ marginBottom: 24 }}>站点名称</div>
<div style={{ fontSize: 32, color: "#a1a1aa" }}>{title}</div>
</div>
),
{ width: 1200, height: 630 }
);
}tsx
// 页面中关联动态OG图片
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
openGraph: {
images: [`/api/og?title=${encodeURIComponent(post.title)}`],
},
};
}SEO 체크리스트
SEO检查清单
| 항목 | 설명 | 필수 |
|---|---|---|
| 페이지별 고유 타이틀 (60자 이내) | O |
| 페이지별 고유 설명 (155자 이내) | O |
| | O |
| 페이지 언어 설정 ( | O |
| 정규 URL 설정 (중복 방지) | O |
| Open Graph | | O |
| Twitter Card | | 권장 |
| JSON-LD | 구조화 데이터 (페이지 유형별) | 권장 |
| sitemap.xml | 전체 페이지 목록 | O |
| robots.txt | 크롤링 규칙 | O |
| 시맨틱 HTML | | O |
이미지 | 모든 의미 있는 이미지에 대체 텍스트 | O |
| HTTPS | SSL 인증서 적용 | O |
| 모바일 친화적 | 반응형 디자인 | O |
| 페이지 속도 | Core Web Vitals 충족 | 권장 |
| 项目 | 说明 | 必填 |
|---|---|---|
| 每个页面的唯一标题(建议60字以内) | O |
| 每个页面的唯一描述(建议155字以内) | O |
| 设置 | O |
| 页面语言设置(如 | O |
| 规范URL设置(避免重复内容) | O |
| Open Graph | 配置 | O |
| Twitter Card | 配置 | 推荐 |
| JSON-LD | 根据页面类型配置结构化数据 | 推荐 |
| sitemap.xml | 包含所有页面的列表 | O |
| robots.txt | 配置爬虫规则 | O |
| 语义化HTML | 正确使用 | O |
图片 | 所有有意义的图片添加替代文本 | O |
| HTTPS | 应用SSL证书 | O |
| 移动端适配 | 响应式设计 | O |
| 页面速度 | 满足Core Web Vitals指标 | 推荐 |
리포트 형식
报告格式
markdown
undefinedmarkdown
undefinedSEO Audit: [대상]
SEO审计: [目标对象]
요약
摘要
- SEO 점수: [N/100]
- 필수 항목 누락: N개
- 권장 항목 누락: N개
- SEO评分: [N/100]
- 必填项遗漏: N个
- 推荐项遗漏: N个
필수 수정
必须修改
[S1] 이슈 제목
[S1] 问题标题
- 항목: [title / description / OG / ...]
- 현재: 없음 또는 현재 값
- 수정안: 코드
- 项目: [title / description / OG / ...]
- 当前状态: 无或当前值
- 修改方案: 代码示例
권장 개선
推荐改进
...
...
통과 항목
通过项目
- ...
undefined- ...
undefined실행 규칙
执行规则
- 인자가 없으면 프로젝트 전체의 SEO 상태를 점검한다
- 인자 시 sitemap.ts 파일을 생성/개선한다
sitemap - 인자 시 동적 OG 이미지 Route Handler를 생성한다
og-image - 파일 경로가 전달되면 해당 페이지의 메타데이터를 분석한다
- 의 전역 메타데이터와 개별 페이지 메타데이터의 상속 구조를 확인한다
layout.tsx - 가 설정되어 있는지 확인하고, 없으면 추가를 안내한다
metadataBase
- 无参数时,检查整个项目的SEO状态
- 传入参数时,生成/优化sitemap.ts文件
sitemap - 传入参数时,创建动态OG图片Route Handler
og-image - 传入文件路径时,分析该页面的元数据
- 检查的全局元数据与单个页面元数据的继承结构
layout.tsx - 确认是否已设置,未设置则提示添加
metadataBase