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,
        }),
      }}
    />
  );
}

설계 포인트

Need Help

도움이 필요하신가요?

주님의교회 PCL 디자인 시스템을 적용하시다가 막히는 부분이 있다면, 다음 경로로 직접 문의하실 수 있습니다.