fe-seo
1
总安装量
1
周安装量
#45511
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-seo
Agent 安装分布
mcpjam
1
claude-code
1
replit
1
junie
1
zencoder
1
Skill 文档
FE SEO & Metadata Optimization
$ARGUMENTS를 ë¶ìíì¬ SEO ê´ë ¨ ë©íë°ì´í°ë¥¼ ìµì ííê±°ë ìì±íë¤.
ë¶ì ì ì°¨
- íì¬ ìí íì : íë¡ì í¸ì ë©íë°ì´í° ì¤ì ì Glob/Readë¡ íì¸íë¤
- SEO ì²´í¬ë¦¬ì¤í¸ ê²ì¬: ìë í목ì ëí´ ëë½ ì¬íì íì¸íë¤
- ê°ì ì ì ì: 구체ì ì¸ ì½ëì í¨ê» ìµì í ë°©ìì ì ìíë¤
- 구í: ì¹ì¸ í ë©íë°ì´í°ë¥¼ ì¶ê°/ìì íë¤
Next.js Metadata API
ì ì Metadata
// 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 (íì´ì§ë³)
// 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 구조í ë°ì´í°
ì¹ì¬ì´í¸ (ì¡°ì§)
// 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)
// 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)
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
const jsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
};
BreadcrumbList
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
// 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ìì ìì±)
// 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ê° ì´ê³¼)
// 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
// 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 ì´ë¯¸ì§ ìì±
// 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 }
);
}
// íì´ì§ìì ëì 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 ì²´í¬ë¦¬ì¤í¸
| í목 | ì¤ëª | íì |
|---|---|---|
<title> |
íì´ì§ë³ ê³ ì íì´í (60ì ì´ë´) | O |
<meta description> |
íì´ì§ë³ ê³ ì ì¤ëª (155ì ì´ë´) | O |
<meta viewport> |
width=device-width, initial-scale=1 |
O |
<html lang> |
íì´ì§ ì¸ì´ ì¤ì (ko) |
O |
<link rel="canonical"> |
ì ê· URL ì¤ì (ì¤ë³µ ë°©ì§) | O |
| Open Graph | og:title, og:description, og:image |
O |
| Twitter Card | twitter:card, twitter:title |
ê¶ì¥ |
| JSON-LD | 구조í ë°ì´í° (íì´ì§ ì íë³) | ê¶ì¥ |
| sitemap.xml | ì ì²´ íì´ì§ ëª©ë¡ | O |
| robots.txt | í¬ë¡¤ë§ ê·ì¹ | O |
| ìë§¨í± HTML | h1~h6 ê³ì¸µ, <main>, <article> ë± |
O |
ì´ë¯¸ì§ alt |
모ë ì미 ìë ì´ë¯¸ì§ì ëì²´ í ì¤í¸ | O |
| HTTPS | SSL ì¸ì¦ì ì ì© | O |
| 모ë°ì¼ ì¹íì | ë°ìí ëìì¸ | O |
| íì´ì§ ìë | Core Web Vitals 충족 | ê¶ì¥ |
리í¬í¸ íì
# SEO Audit: [ëì]
## ìì½
- SEO ì ì: [N/100]
- íì í목 ëë½: Nê°
- ê¶ì¥ í목 ëë½: Nê°
## íì ìì
### [S1] ì´ì ì 목
- **í목**: [title / description / OG / ...]
- **íì¬**: ìì ëë íì¬ ê°
- **ìì ì**: ì½ë
## ê¶ì¥ ê°ì
...
## íµê³¼ í목
- ...
ì¤í ê·ì¹
- ì¸ìê° ìì¼ë©´ íë¡ì í¸ ì ì²´ì SEO ìí를 ì ê²íë¤
sitemapì¸ì ì sitemap.ts íì¼ì ìì±/ê°ì íë¤og-imageì¸ì ì ëì OG ì´ë¯¸ì§ Route Handler를 ìì±íë¤- íì¼ ê²½ë¡ê° ì ë¬ëë©´ í´ë¹ íì´ì§ì ë©íë°ì´í°ë¥¼ ë¶ìíë¤
layout.tsxì ì ì ë©íë°ì´í°ì ê°ë³ íì´ì§ ë©íë°ì´í°ì ìì 구조를 íì¸íë¤metadataBaseê° ì¤ì ëì´ ìëì§ íì¸íê³ , ìì¼ë©´ ì¶ê°ë¥¼ ìë´íë¤