SEO๋ฅผ ์ํ Next.js ๋ฉํ๋ฐ์ดํฐ ์ค์ ๋ฐฉ๋ฒ
Next.js๋ฅผ ์ฌ์ฉํ๋ ๋ํ์ ์ธ ์ด์ ์ธ SEO๋ฅผ ๋ธ๋ก๊ทธ์ ์ ์ฉํ๋ฉด์ ๊ฒช์ ์ฝ์ง๊ธฐ๋ฅผ ๊ธฐ๋ก ๋ฐ ๊ณต์ ํ๊ณ ์ ํฌ์คํธ๋ฅผ ์์ฑํ๋ค.
What is SEO? SEO stands for Search Engine Optimization. The goal of SEO is to create a strategy that will increase your rankings position in search engine results. The higher the ranking, the more organic traffic to your site, which ultimately leads to more business for you!
๋ฉํ๋ฐ์ดํฐ ์ค์
next js page router ์์๋ next/head์ Head ํ๊ทธ๋ด์ ์ ์ํด์ ์ค์ ํด์ผ ํ์ง๋ง app router์์๋ layout ๋๋ page์์ metadata๋
generateMetadata
๋ฅผ export ํ๋ฉด๋๋ค.
// ์ ์ ๋ฉํ๋ฐ์ดํฐ ์์ฑ
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '...',
description: '...',
};
export default function Page() {}
// ๋์ ๋ฉํ๋ฐ์ดํฐ ์์ฑ
import type { Metadata, ResolvingMetadata } from 'next';
type Props = {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
// read route params
const id = params.id;
// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json());
// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || [];
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
};
}
export default function Page({ params, searchParams }: Props) {}
๋์ ์ผ๋ก ์์ฑํ๋ ๊ฒฝ์ฐ args๋ก params
, searchParams
, parent
(๋ถ๋ชจ์ ๋ฉํ๋ฐ์ดํฐ)๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
favicon๊ณผ opengraph image ์ฐ๋
create next app ๋ช ๋ น์ด๋ก ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ฉด vercel์ ๋ก๊ณ ๊ฐ ๊ธฐ๋ณธ ํ๋น์ฝ์ผ๋ก ์ค์ ๋์ด ์๋ค. ์ฐ์ ํ๋น์ฝ์ ๋ณ๊ฒฝํ๋ ค๋ฉด ํ๋น์ฝ์ด ์์ด์ผ ํ๋๋ฐ, ๋๋ favicon.io ์ฌ๊ธฐ์ ํ๋น์ฝ์ ์์ฑํ๋ค.
์ ๋นํ ํ๋น์ฝ์ ๋ง๋ค๊ณ ๋ค์ด๋ก๋๋ฅผ ํ๋ฉด android, apple, favicon ์ด๋ฆ๋ถํฐ png, ico ํ์ฅ์๋ช ํ์ผ, site.webmanifest ๋ฑ ์ฌ๋ฌ ํ์ผ์ด ํจ๊ป์๊ธด๋ค.
์ฐ์ ํ๋น์ฝ์ ๋ณ๊ฒฝํด๋ณด์
๊ธฐ๋ณธ์ ์ผ๋ก app
๋๋ ํ ๋ฆฌ ์์ favicon.ico ๋ผ๋ ํ์ผ์ด ์์ํ
๋ฐ, ํด๋น ํ์ผ์ ์์ฑํ favicon.ico๋ก ๊ต์ฒดํด๋ ๋์ง๋ง, metadata๋ก ์ ๋ฌํด๋ ํ๋น์ฝ์ ์ ์ฉํ ์ ์๋ค.
๋๋ /public/favicon ํ์๋ก ๊ด๋ จ ํ์ผ์ ๋ชจ๋ ๋ฃ์ด์ฃผ๊ณ ๋ฐฐ์ด ํํ๋ก ๋ง๋ค์ด์ layout์ ๋ฉํ๋ฐ์ดํฐ > icons ๋ก ๋ฃ์ด์คฌ๋ค.
// /constant/metadata.ts
export const METADATA_ICONS: Metadata["icons"] = [
{
url: "/favicon/favicon-16.png",
sizes: "16x16",
type: "image/png",
},
{
...
},
]
// /app/layout.tsx
export const metadata: Metadata = {
...DEFAULT_META,
icons: METADATA_ICONS,
};
์ฌ๊ธฐ๊น์ง ์ ์ฉํ๋ฉด ํ๋น์ฝ ์์ด์ฝ์ด ๋ณ๊ฒฝ๋์ด์๋ค.
์ด์ opengraph๋ฅผ ์ ์ฉํด๋ณด์. ๋ง์ฝ opengraph ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด ์๋์ฒ๋ผ url ๊ฒฝ๋ก๋ก metadata์์ ์ค์ ํ ์ ์๋ค.
// ๋ฉํ๋ฐ์ดํฐ opengraph ์ด๋ฏธ์ง ์ค์ ๋ฐฉ๋ฒ
export const metadata: Metadata = {
openGraph: {
images: [
{
url: "/public/opengraph-image.png",
alt: "...",
type: "image/png",
width: 000,
height: 000,
},
],
},
};
๊ทธ๋ฌ๋ ๋๋ opengraph ์ด๋ฏธ์ง๊ฐ ์์ด์ next.js๊ฐ ์ ๊ณตํ๋ ๋์ ์ผ๋ก opengraph, twitter card ๋ฑ ์์ ๋ฏธ๋์ด ์ด๋ฏธ์ง ์์ฑ์์ธ ImageResponse ๋ฅผ ์ฌ์ฉํด์ opengraph ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค๊ธฐ๋ก ํ๋ค.
14๋ฒ์ ์ด์ ์๋ next/server
์์ import ํ ์ ์์๊ณ , 14๋ฒ์ ๋ถํด next/og
์์ import ํ ์ ์๋ค.
์์๋ ์ฌ๊ธฐ ๋์์๋ค. opengraph๋ฅผ ์ฌ์ฉํ ๊ฒฝ๋ก์ opengraph-image.tsx
๋ก ๋ง๋ค๋ฉด ๋๋ค.
๊ทธ๋ฐ๋ฐ ๋๋ root์ opengraph-image.tsx๋ฅผ ๋ง๋ค์๋๋ฐ, ํ์ routes์๋ ์ ์ฉ์ด ์๋๋ค. ๊ทธ๋์ ์ง์ ๊ฒฝ๋ก๋ฅผ ๋ฃ์ด์ฃผ๊ธฐ ์ํด vercel ๋ฌธ์์์ ์ ๊ณตํ๋ ํํ์ธ api routes๋ก ๋ง๋ค๊ธฐ๋ก ํ๋ค.
๋ฐฉ๋ฒ์ opengraph-image.tsx๋ก ๋ง๋๋๊ฑฐ๋ ๊ฑฐ์ ๋์ผํ๋ฐ, ์ฐจ์ด์ ์ next.js์์ ์ ๊ณตํ๋ api routes๋ฅผ ์ฌ์ฉํด์ ๋ง๋๋ ์ ์ด ๋ค๋ฅด๋ค.
// /api/og/route.tsx
import { ImageResponse } from 'next/og';
// App router includes @vercel/og.
// No need to install it.
export async function GET() {
return new ImageResponse(
(
<div
style={{
fontSize: 40,
color: 'black',
background: 'white',
width: '100%',
height: '100%',
padding: '50px 200px',
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
}}
>
๐ Hello
</div>
),
{
width: 1200,
height: 630,
},
);
}
์ด๋ ๊ฒ ๊ตฌํํ api ๊ฒฝ๋ก๋ฅผ ๋ฉํ๋ฐ์ดํฐ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก๋ก ์ค์ ํ๋ฉด ๋๋ค.
const metadata: Metadata = {
...
openGraph: {
images: [
{
url: "/api/og",
alt: "Shimyuseob's blog og image",
type: "image/png",
width: 1200,
height: 630,
},
],
...
๋ด ๋ธ๋ก๊ทธ๋ฅผ ์์๋ก ๋ณด๋ฉด https://www.shimyuseob.xyz/api/og ๊ฒฝ๋ก์ opengraph ์ด๋ฏธ์ง๋ฅผ ํ์ธํ ์ ์๋ค.
og๊ฐ ์ ์ค์ ๋๋์ง๋ head์ og:image ๋ฉํํ๊ทธ๋ฅผ ๋ณด๊ฑฐ๋, https://www.opengraph.xyz/ ์ด๊ณณ์์๋ ํ์ธํ ์ ์๋ค.
SEO๋ฅผ ๋ค ์ค์ ํ๋ฉด ๊ตฌ๊ธ ์์น์ฝ์์์ ์์ธ์์ฑ์ ์์ฒญํ๊ฑฐ๋ ํ์ธํ ์ ์๋ค.
JSON-LD
๋ฉํ๋ฐ์ดํฐ ๋ฌธ์๋ฅผ ๋ณด๋ฉด JSON-LD ์ค์ ๋ ๋์์๋ค.
JSON-LD๋ ๊ฒ์ ์์ง์ด ์ฝํ ์ธ ๋ฅผ ์ดํดํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ ํ์
์ธ๋ฐ, ํ์ฌ ํ๋ก์ ํธ์ ์ค์ ํด๋์ง๋ง ๋ธ๋ก๊ทธ์๋ JSON-LD๋ ์ค์ ํ์ง ์์๋ค.
๋์ค์ ํฌ์คํธ๋ ๋ง์์ง๊ณ , ์์ธ์ด ์ ์๋๋ ๊ฒ ๊ฐ์ผ๋ฉด ์ถ๊ฐํด๋ด๋ ์ข์ ๊ฒ ๊ฐ๋ค.