06 · Developer Guide · 레시피
SEO · 사이트맵 · OG 이미지SEO Metadata
교회 홈페이지는 검색 노출이 곧 새신자 유입 경로입니다.
메타데이터 · 사이트맵 · OG 이미지 · 구조화 데이터 4종을 한 번에 갖춥니다.
언제 쓰는가
- 모든 페이지 — 카카오톡 공유 미리보기, 구글 검색 노출 기본
- 게시물 상세 — 글마다 다른 제목·설명·표지로 SNS 공유 품질 향상
- 새 도메인 첫 배포 직후 — 사이트맵 등록으로 색인 가속
① 사이트 공통 메타 — app/layout.tsx
app/layout.tsxtsx
// app/layout.tsx — 사이트 공통 메타데이터
import type { Metadata } from "next";
const SITE_URL = "https://pcldesign.kr";
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: "주님의교회 PCL",
template: "%s · 주님의교회 PCL",
},
description:
"복음의 가치로 세상을 섬기는 주님의교회 PCL. 예배 · 양육 · 선교 안내와 새소식, 주보, 함즐함울, 큐티를 한곳에서.",
applicationName: "주님의교회 PCL",
authors: [{ name: "주님의교회 PCL" }],
generator: "Next.js",
keywords: ["주님의교회", "PCL", "예배", "주보", "큐티", "함즐함울", "교회"],
openGraph: {
type: "website",
locale: "ko_KR",
siteName: "주님의교회 PCL",
images: [{ url: "/og/default.png", width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
images: ["/og/default.png"],
},
alternates: {
canonical: "/",
},
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true, "max-image-preview": "large" },
},
icons: {
icon: [
{ url: "/favicon.ico" },
{ url: "/icon-32.png", sizes: "32x32" },
{ url: "/icon-192.png", sizes: "192x192" },
],
apple: "/apple-icon.png",
},
};② 페이지별 동적 메타 — generateMetadata
app/(site)/site/yard/news/[id]/page.tsxtsx
// app/(site)/site/yard/news/[id]/page.tsx — 게시물별 메타
import { notFound } from "next/navigation";
import { getNewsPost } from "@/lib/preview/news";
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const { id } = await params;
const post = await getNewsPost(id);
if (!post) return {};
return {
title: post.title,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
type: "article",
publishedTime: post.date,
images: post.cover ? [{ url: post.cover }] : undefined,
},
alternates: {
canonical: `/site/yard/news/${id}`,
},
};
}③ 사이트맵 — app/sitemap.ts
app/sitemap.tsts
// app/sitemap.ts — 동적 사이트맵
import type { MetadataRoute } from "next";
import { newsPosts } from "@/lib/preview/news";
import { bulletinIssues } from "@/lib/preview/bulletin";
import { hamjulIssues } from "@/lib/preview/hamjul";
import { qtIssues } from "@/lib/preview/qt";
const SITE_URL = "https://pcldesign.kr";
export default function sitemap(): MetadataRoute.Sitemap {
const STATIC = [
"", "/site", "/site/community/about",
"/site/worship/sunday", "/site/yard/news",
// ... 117개 정적 경로
];
const posts = [
...newsPosts.map((p) => ({
url: `${SITE_URL}/site/yard/news/${p.id}`,
lastModified: new Date(p.date),
changeFrequency: "yearly" as const,
priority: 0.6,
})),
...bulletinIssues.map((b) => ({
url: `${SITE_URL}/site/yard/bulletin/${b.slug}`,
lastModified: new Date(b.date),
changeFrequency: "weekly" as const,
priority: 0.7,
})),
...hamjulIssues.map((h) => ({
url: `${SITE_URL}/site/yard/happy-board/${h.no}`,
lastModified: new Date(h.date),
changeFrequency: "monthly" as const,
priority: 0.6,
})),
...qtIssues.map((q) => ({
url: `${SITE_URL}/site/nurture/qt/${q.slug}`,
lastModified: new Date(q.date),
changeFrequency: "yearly" as const,
priority: 0.5,
})),
];
return [
...STATIC.map((path) => ({
url: `${SITE_URL}${path}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: path === "" ? 1 : 0.8,
})),
...posts,
];
}④ robots.txt — app/robots.ts
app/robots.tsts
// app/robots.ts
import type { MetadataRoute } from "next";
const SITE_URL = "https://pcldesign.kr";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/site/admin/", "/site/my/", "/api/", "/_next/"],
},
{
userAgent: ["GPTBot", "ClaudeBot", "PerplexityBot"],
allow: "/",
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
host: SITE_URL,
};
}⑤ 동적 OG 이미지 — opengraph-image.tsx
app/(site)/site/yard/news/[id]/opengraph-image.tsxtsx
// app/(site)/site/yard/news/[id]/opengraph-image.tsx
// 게시물별 OG 이미지를 빌드 타임에 동적 생성 (vercel/og)
import { ImageResponse } from "next/og";
import { getNewsPost } from "@/lib/preview/news";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({ params }: { params: { id: string } }) {
const post = await getNewsPost(params.id);
if (!post) return new Response("Not found", { status: 404 });
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: 80,
background: "linear-gradient(135deg, #fafaf7 0%, #fff 100%)",
}}
>
<div style={{ fontSize: 28, color: "#0046aa", letterSpacing: 4 }}>
주님의교회 PCL · 새소식
</div>
<div
style={{
fontSize: 64,
fontWeight: 700,
color: "#1a1a1a",
lineHeight: 1.25,
}}
>
{post.title}
</div>
<div style={{ fontSize: 28, color: "#666" }}>
{post.date} · pcldesign.kr
</div>
</div>
),
size,
);
}⑥ 구조화 데이터 (JSON-LD)
tsx
// 구조화된 데이터 (JSON-LD) — 게시물 페이지에 삽입
export function NewsArticleSchema({ post }: { post: NewsPost }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "NewsArticle",
headline: post.title,
datePublished: post.date,
author: { "@type": "Organization", name: "주님의교회 PCL" },
publisher: {
"@type": "Organization",
name: "주님의교회 PCL",
logo: { "@type": "ImageObject", url: "/brand/pcl-logo.svg" },
},
image: post.cover,
}),
}}
/>
);
}