원클릭으로
seo-metadata
Optimize portfolio websites for search engines with Next.js Metadata API, structured data, Open Graph, and performance SEO.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Optimize portfolio websites for search engines with Next.js Metadata API, structured data, Open Graph, and performance SEO.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | seo-metadata |
| description | Optimize portfolio websites for search engines with Next.js Metadata API, structured data, Open Graph, and performance SEO. |
| author | Jaivish Chauhan @ GDG SSIT |
| version | 1.0.0 |
| url | https://github.com/JaivishChauhan/vibecoding-starter |
SEO for portfolios isn't about ranking #1 for "web developer"—it's about discoverability when someone Googles your name, sharing beautiful previews on social media, and ensuring your projects appear professionally in search results.
// app/layout.tsx
import type { Metadata, Viewport } from "next";
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#0a0a0a" },
],
width: "device-width",
initialScale: 1,
maximumScale: 5,
};
export const metadata: Metadata = {
metadataBase: new URL("https://yourportfolio.com"),
title: {
default: "John Doe | Full-Stack Developer",
template: "%s | John Doe", // For child pages: "Projects | John Doe"
},
description:
"Full-stack developer specializing in React, Next.js, and TypeScript. Building beautiful, performant web experiences.",
keywords: [
"John Doe",
"Web Developer",
"Full-Stack Developer",
"React Developer",
"Next.js",
"TypeScript",
"Frontend Developer",
"Portfolio",
],
authors: [{ name: "John Doe", url: "https://yourportfolio.com" }],
creator: "John Doe",
publisher: "John Doe",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
// Open Graph (Facebook, LinkedIn, Discord)
openGraph: {
type: "website",
locale: "en_US",
url: "https://yourportfolio.com",
siteName: "John Doe Portfolio",
title: "John Doe | Full-Stack Developer",
description:
"Full-stack developer specializing in React, Next.js, and TypeScript.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "John Doe - Full-Stack Developer",
},
],
},
// Twitter Card
twitter: {
card: "summary_large_image",
site: "@johndoe",
creator: "@johndoe",
title: "John Doe | Full-Stack Developer",
description:
"Full-stack developer specializing in React, Next.js, and TypeScript.",
images: ["/og-image.png"],
},
// Verification
verification: {
google: "your-google-verification-code",
yandex: "your-yandex-verification-code",
},
// Alternate languages (if applicable)
alternates: {
canonical: "https://yourportfolio.com",
languages: {
"en-US": "https://yourportfolio.com",
"es-ES": "https://yourportfolio.com/es",
},
},
// App links
other: {
"msapplication-TileColor": "#0a0a0a",
},
};
// app/projects/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Projects", // Becomes "Projects | John Doe" via template
description:
"Explore my portfolio of web applications, open source projects, and client work.",
openGraph: {
title: "Projects | John Doe",
description: "Explore my portfolio of web applications and client work.",
images: ["/og-projects.png"],
},
};
export default function ProjectsPage() {
return <main>...</main>;
}
// app/projects/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
import { getProjectBySlug, getAllProjects } from "@/lib/projects";
import { notFound } from "next/navigation";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise<Metadata> {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) {
return { title: "Project Not Found" };
}
// Optionally access parent metadata
const previousImages = (await parent).openGraph?.images || [];
return {
title: project.title,
description: project.description,
openGraph: {
title: `${project.title} | John Doe`,
description: project.description,
type: "article",
publishedTime: project.date,
authors: ["John Doe"],
images: [
{
url: project.ogImage || project.image,
width: 1200,
height: 630,
alt: project.title,
},
...previousImages,
],
},
twitter: {
card: "summary_large_image",
title: project.title,
description: project.description,
images: [project.ogImage || project.image],
},
};
}
// Generate static paths for all projects
export async function generateStaticParams() {
const projects = await getAllProjects();
return projects.map((project) => ({
slug: project.slug,
}));
}
export default async function ProjectPage({ params }: Props) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) notFound();
return <article>...</article>;
}
// app/blog/[slug]/page.tsx
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],
tags: post.tags,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
// Article-specific
other: {
"article:published_time": post.publishedAt,
"article:author": post.author.name,
"article:section": post.category,
"article:tag": post.tags.join(", "),
},
};
}
// app/about/page.tsx
import Script from "next/script";
const personJsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: "John Doe",
url: "https://yourportfolio.com",
image: "https://yourportfolio.com/john-doe.jpg",
sameAs: [
"https://twitter.com/johndoe",
"https://linkedin.com/in/johndoe",
"https://github.com/johndoe",
],
jobTitle: "Full-Stack Developer",
worksFor: {
"@type": "Organization",
name: "Freelance",
},
knowsAbout: ["React", "Next.js", "TypeScript", "Node.js", "PostgreSQL"],
};
export default function AboutPage() {
return (
<>
<Script
id="person-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
<main>...</main>
</>
);
}
// app/layout.tsx
import Script from "next/script";
const websiteJsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "John Doe Portfolio",
url: "https://yourportfolio.com",
description:
"Full-stack developer portfolio showcasing web applications and projects.",
author: {
"@type": "Person",
name: "John Doe",
},
potentialAction: {
"@type": "SearchAction",
target: "https://yourportfolio.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Script
id="website-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
{children}
</body>
</html>
);
}
// components/project-schema.tsx
import Script from "next/script";
interface ProjectSchemaProps {
project: {
title: string;
description: string;
image: string;
url: string;
dateCreated: string;
technologies: string[];
};
}
export function ProjectSchema({ project }: ProjectSchemaProps) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "CreativeWork",
name: project.title,
description: project.description,
image: project.image,
url: project.url,
dateCreated: project.dateCreated,
author: {
"@type": "Person",
name: "John Doe",
url: "https://yourportfolio.com",
},
keywords: project.technologies.join(", "),
};
return (
<Script
id={`project-schema-${project.title}`}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
// components/article-schema.tsx
export function ArticleSchema({ post }: { post: BlogPost }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt || post.publishedAt,
author: {
"@type": "Person",
name: post.author.name,
url: "https://yourportfolio.com",
},
publisher: {
"@type": "Person",
name: "John Doe",
logo: {
"@type": "ImageObject",
url: "https://yourportfolio.com/logo.png",
},
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://yourportfolio.com/blog/${post.slug}`,
},
};
return (
<Script
id={`article-schema-${post.slug}`}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
// components/breadcrumb-schema.tsx
interface BreadcrumbItem {
name: string;
url: string;
}
export function BreadcrumbSchema({ items }: { items: BreadcrumbItem[] }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: item.url,
})),
};
return (
<Script
id="breadcrumb-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
// Usage
<BreadcrumbSchema
items={[
{ name: "Home", url: "https://yourportfolio.com" },
{ name: "Projects", url: "https://yourportfolio.com/projects" },
{
name: project.title,
url: `https://yourportfolio.com/projects/${project.slug}`,
},
]}
/>;
Place in /public/og-image.png (1200x630px)
// 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 } = new URL(request.url);
const title = searchParams.get("title") || "John Doe";
const description = searchParams.get("description") || "Full-Stack Developer";
return new ImageResponse(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
backgroundImage:
"radial-gradient(circle at 25% 25%, #1a1a2e 0%, transparent 50%)",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
padding: "40px 80px",
}}
>
<h1
style={{
fontSize: 72,
fontWeight: "bold",
color: "white",
lineHeight: 1.1,
marginBottom: 20,
}}
>
{title}
</h1>
<p
style={{
fontSize: 32,
color: "#a1a1aa",
marginTop: 0,
}}
>
{description}
</p>
</div>
{/* Footer branding */}
<div
style={{
position: "absolute",
bottom: 40,
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<span style={{ fontSize: 24, color: "#71717a" }}>
yourportfolio.com
</span>
</div>
</div>,
{
width: 1200,
height: 630,
},
);
}
// app/projects/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) return { title: "Not Found" };
const ogUrl = new URL("/api/og", "https://yourportfolio.com");
ogUrl.searchParams.set("title", project.title);
ogUrl.searchParams.set("description", project.description);
return {
title: project.title,
description: project.description,
openGraph: {
images: [
{
url: ogUrl.toString(),
width: 1200,
height: 630,
alt: project.title,
},
],
},
};
}
// app/sitemap.ts
import { MetadataRoute } from "next";
import { getAllProjects } from "@/lib/projects";
import { getAllPosts } from "@/lib/blog";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://yourportfolio.com";
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/projects`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.5,
},
];
// Dynamic project pages
const projects = await getAllProjects();
const projectPages: MetadataRoute.Sitemap = projects.map((project) => ({
url: `${baseUrl}/projects/${project.slug}`,
lastModified: new Date(project.updatedAt || project.date),
changeFrequency: "monthly" as const,
priority: 0.7,
}));
// Dynamic blog pages
const posts = await getAllPosts();
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt || post.publishedAt),
changeFrequency: "weekly" as const,
priority: 0.6,
}));
return [...staticPages, ...projectPages, ...blogPages];
}
// app/robots.ts
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/"],
},
],
sitemap: "https://yourportfolio.com/sitemap.xml",
};
}
// app/manifest.ts
import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "John Doe Portfolio",
short_name: "John Doe",
description: "Full-Stack Developer Portfolio",
start_url: "/",
display: "standalone",
background_color: "#0a0a0a",
theme_color: "#8b5cf6",
icons: [
{
src: "/icon-192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icon-512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/icon-512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
};
}
app/
├── favicon.ico # 32x32 favicon
├── icon.png # App icon (auto-detected)
├── apple-icon.png # Apple touch icon
├── opengraph-image.png # Default OG image
└── twitter-image.png # Default Twitter image
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
optimizeCss: true, // CSS optimization
},
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Compress responses
compress: true,
};
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
{/* Preload fonts */}
<link
rel="preload"
href="/fonts/CalSans-SemiBold.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* Preconnect to external domains */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
{/* DNS prefetch for analytics */}
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
</head>
<body>{children}</body>
</html>
);
}
Build accessible portfolio websites following WCAG guidelines with proper semantics, keyboard navigation, screen reader support, and inclusive design patterns.
Implement analytics, error monitoring, and performance tracking with Vercel Analytics, Google Analytics, Sentry, and custom event tracking.
Build server-side APIs using Next.js Server Actions and API routes.
Build production-ready contact forms with React Hook Form, Zod validation, Server Actions, and email services like Resend.
Manage portfolio content with MDX, Contentlayer, or local JSON for projects, blog posts, and dynamic content.
Implement robust dark mode and theming systems with next-themes, CSS custom properties, and Tailwind CSS for seamless user experience.