[Nextjs] Dynamic metadata tag ์์ฑํ๊ธฐ (SEO ์งํ 54 โก๏ธ 91)
nextjs๋ก ํด๋ณด๊ณ ์ถ์๋ ๊ฒ์ด ์๋ค๋ฉด Metadata API ๋ฅผ ํ์ฉํด์ ๊ฐ๋จํ๊ฒ metadata tag๋ฅผ ๋ฌ์์ฃผ๋ ๊ฒ ์๋๊น ์ถ๋ค.์ค๋์ ๊ทธ ์ค ํ์ด์ง ํ๋์ ๋ฐ๋ผ ๋ค์ด๋ด๋ฏนํ๊ฒ ๋ฉํํ๊ทธ๋ฅผ ์์ฑํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋
pyotato-dev.tistory.com
์ด์ ์๋ /region ๊ฒฝ๋ก์ ํ์ด์ง๋ค์ ๋ํ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ์ฌ์คฌ๋ค.
ํ์ง๋ง ๋น์ ์ค๋ณต๋๋ ๋ถ๋ถ์ด ๋ง์ ๊ฑฐ ๊ฐ์์ ์ข ๋ ๊ฐ๊ฒฐํ๊ฒ ํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋,
์์์ ํ์ด์ง์ ๊ณต์ ํ ๋ฉํ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ธ ์ ์๋ค๋ ์ ์ด๋ค.
/regions/[regionId] ์์ ์๋ฅผ ๋ค๋ฉด,
/regions/๋ํ๋ฏผ๊ตญ์ ํ์ด์ง๊ฐ ์๊ณ ,
/regions/[regionId]/articles/[articleId] ์ /regions/[์ง์ญ์์ด๋]/articles/[์์ดํด ์์ด๋] ๊ฐ ์๊ณ (restaurants ์ attraction์ ๋์ผ)
๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง์ผ๋ก regions/[regionId]/location/[locationId]์ฒ๋ผ regions/๋ํ๋ฏผ๊ตญ/location/[์ ์ฃผ๋์ ์์ด๋]๊ฐ ์๋ค.
๊ทผ๋ฐ ๊ฐ๊ฐ์ metaData์์ ๋ค์๊ณผ ๊ฐ์ ๊ฐ๋ค์ ๊ณตํต์ด๋ค.
- type: 'website'
- siteName: 'Tripie'
- title : 'โ๏ธTripie' ๋ผ๋ ์ ๋ฏธ์ฌ๊ฐ ์์ด์ผ ํ๋ค.
๊ทธ ์ธ์ title๊ณผ description์ ํ์ด์ง์ ๋ฐ์ดํฐ์ ๋ฐ๋ผ ๋ฌ๋ผ์ ธ์ผ ํ๋ค.
์ด๋ฅผ ๋ฐ์ํ๋ฉด ๊ณตํต์ผ๋ก sharedMetaData ํจ์๋ฅผ /region ์ ์์ฑํด ์ฃผ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
title์ template์ ํ์ฉํ๋ฉด ์ ๋ชฉ์ ๋์ ํญ์ โ๏ธTripie๊ฐ ๋ถ๋๋ก ํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ default ์ค์ ์ ํตํด fallback ์ ์ ๊ณตํ ์ ์๋ค.
import API from 'constants/api-routes';
import { Metadata } from 'next';
// http://nextjs.org/docs/app/api-reference/functions/generate-metadata#template
export const sharedMetaData: Metadata['openGraph'] = {
title: {
template: '%s | โ๏ธTripie',
default: 'โ๏ธTripie',
},
type: 'website',
siteName: 'Tripie',
description: 'AI enhanced trip planner',
images: [API.BASE_URL + '/tripie-image.png'],
};
๊ทธ๋ฆฌ๊ณ /regions ์ layout.tsx ์ importํด์ ์ผ๋ฐ ๋ฉํ๋ฐ์ดํฐ์ og ๋ฉํ๋ฐ์ดํฐ์ ์ถ๊ฐํด ์ฃผ๋ฉด
export const metadata: Metadata = {
title: sharedMetaData?.title,
description: sharedMetaData?.description,
openGraph: sharedMetaData,
};
https://tripie-mauve.vercel.app/regions์ ์ ์ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ํดํธ ๋ฉํํ๊ทธ๊ฐ ๋ณด์ธ๋ค.
โ๏ธTripie
AI enhanced trip planner
tripie-mauve.vercel.app
ํ์ง๋ง ์ถ๊ฐ์ ์ผ๋ก ๊ฒ์์ ๋ํ ์ ๋ณด๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ก ์ ๊ณตํ๊ณ ์ถ์ด page.tsx์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํด์ฃผ๊ณ , ์ด์ ์์ฑํ๋
layout.tsx์ ์ฝ๋๋ฅผ ์ ๊ฑฐํด์ฃผ๋ฉด https://tripie-mauve.vercel.app/regions์ ๋ค์๊ณผ ๊ฐ์ด ๊ณต์ ํ ์ ์๋ค. (์์ธ์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง ํฐ์คํ ๋ฆฌ ์๋ํฐ์์๋ ์์ ์ด์ ์ ์นด๋๋ง ๋ณด์)
โ๏ธTripie
AI enhanced trip planner
tripie-mauve.vercel.app
export async function generateMetadata(): Promise<Metadata> {
const regions = Object.keys(TRIPIE_REGION_BY_LOCATION);
const title = `๋์ ๋ณ ์ฌํ ์ ๋ณด ์ดํด๋ณด๊ธฐ`;
const description = `${regions
.slice(0, 3)
.map(key => {
return `โ๏ธ ${key} | ${TRIPIE_REGION_BY_LOCATION[key as keyof typeof TRIPIE_REGION_BY_LOCATION]}`;
})
.join('\n')}...\n๐ ํธ๋ฆฌํผ์์ ์์ธํ ์์๋ณด๊ธฐ!`;
return {
title,
description,
openGraph: {
...sharedMetaData,
title,
description,
url: `${API.BASE_URL}${ROUTE.REGIONS.href}`,
},
};
}
/regions/๋ํ๋ฏผ๊ตญ
export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const regionId = (await params).regionId;
const currentRegionId = decodeURI(regionId);
const selected = TRIPIE_REGION_BY_LOCATION[currentRegionId as keyof typeof TRIPIE_REGION_BY_LOCATION];
const selectedRegion = Object.keys(TRIPIE_REGION_IDS).filter(
item => TRIPIE_REGION_IDS[item as keyof typeof TRIPIE_REGION_IDS] === selected[0]
)?.[0];
const dynamicBlurDataUrl = await getRegionArticles(selectedRegion);
const previousImages = (await parent).openGraph?.images || [];
const title = `๋์ ๋ณ ์ฌํ ์ ๋ณด > ${currentRegionId}`;
const sneakPeak = dynamicBlurDataUrl.slice(0, 5);
const description = `${currentRegionId} ์ฌํ ์ ๋ณด\n ${sneakPeak
.map(item => {
return `โ๏ธ ${item.source.title} | ${item.source.summary}`;
})
.join('\n')}\n...\n๐ ํธ๋ฆฌํผ์์ ์์ธํ ์์๋ณด๊ธฐ!`;
return {
title,
description,
openGraph: {
...sharedMetaData,
title,
description,
images: [...sneakPeak.map(item => item.source.image.sizes.full.url), ...previousImages],
url: `${API.BASE_URL}${ROUTE.REGIONS.href}/${selectedRegion}`,
},
};
}
https://tripie-mauve.vercel.app/regions/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD์ ์ ์ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ํํฐ ํ ์ ๋ณด๋ก ๋ฎ์ด์ด ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํ์ธํ ์ ์๋ค.
๋์ ๋ณ ์ฌํ ์ ๋ณด > ๋ํ๋ฏผ๊ตญ | โ๏ธTripie
๋ํ๋ฏผ๊ตญ ์ฌํ ์ ๋ณด โ๏ธ ์์ฆ SNS์์ ํซํ ๊ฐํ ์นดํ | ํ๊ฒฝ๊ณผ ๊ฐ์ฑ์ ์ฆ๊ธธ ์ ์๋ ๊ฐํ์ ์นดํ๋ค โ๏ธ ์์ฆ ๊ฐํ ์ฌํ์์๋ ๋ฌด์์ ํ ๊น? | ๋ปํ์ง ์๊ณ ์๋ค๋ฅด๊ฒ ๊ฐํ ์ฆ๊ธฐ๊ธฐ โ๏ธ ์ธ์์ท์
tripie-mauve.vercel.app
/regions/๋ํ๋ฏผ๊ตญ/locationId/์ ์ฃผId
export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const regionId = (await params).regionId;
const locationId = (await params).locationId;
const currentRegionId = decodeURI(regionId);
const dynamicBlurDataUrl = await getRegionArticles(locationId);
const currentCity = TRIPIE_REGION_IDS[locationId as keyof typeof TRIPIE_REGION_IDS];
const previousImages = (await parent).openGraph?.images || [];
const title = `๋์ ๋ณ ์ฌํ ์ ๋ณด > ${currentRegionId} > ${currentCity}`;
const sneakPeak = dynamicBlurDataUrl.slice(0, 5);
const description = `${currentRegionId} > ${currentCity} ์ฌํ ์ ๋ณด\n${sneakPeak
.map(item => {
return `โ๏ธ ${item.source.title} | ${item.source.summary}`;
})
.join('\n')}\n...\n๐ ํธ๋ฆฌํผ์์ ์์ธํ ์์๋ณด๊ธฐ!`;
return {
title,
description,
openGraph: {
...sharedMetaData,
title,
description,
images: [...sneakPeak.map(item => item.source.image.sizes?.full.url), ...previousImages], // ์ ์ด๋ฏธ์ง ๋จผ์
url: `${API.BASE_URL}${ROUTE.REGIONS.href}/${currentRegionId}/location/${currentCity}`,
},
};
}
https://tripie-mauve.vercel.app/regions/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD/location/759174cc-0814-4400-a420-5668a0517edd ๋ก ์ ์ํ๋ฉด ๋ํ๋ฏผ๊ตญ> ์ ์ฃผ๋ก ๋ฎ์์ด ๋ฉํ๋ฐ์ดํฐ๊ฐ ์ ๊ณต์ ๋๋ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๋์ ๋ณ ์ฌํ ์ ๋ณด > ๋ํ๋ฏผ๊ตญ > ์ ์ฃผ | โ๏ธTripie
๋ํ๋ฏผ๊ตญ > ์ ์ฃผ ์ฌํ ์ ๋ณด โ๏ธ ์ ์ฃผ ์ฃผ์ ์ง์ญ๊ณผ ๋ช ์ ์๊ฐ | ์ง์ญ๋ณ ์ฌํ ํ๊น์ง ํ๋์ ๋ณด๊ธฐ โ๏ธ ์ ์ฃผ ์ฌํ ์ฝ์ค ์ง๋ ๋ฐฉ๋ฒ | ์ ์๋๋ก ์ฌํ ๊ฐ๋ฅํ 3๋ฐ 4์ผ ์ถ์ฒ์ฝ์ค โ๏ธ ์ ์ฃผ ์ฌํ ๊ฟํ ๊ฐ
tripie-mauve.vercel.app
๋ง์ฐฌ๊ฐ์ง๋ก /regions/๋ํ๋ฏผ๊ตญ/locationId/ํฌํญ*์๋Id ๋ ํด๋น ํ์ด์ง์ ๋ฐ์ดํฐ๋ก ๋ฎ์ด์ด ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํ์ธํ ์ ์๋ค.
๋์ ๋ณ ์ฌํ ์ ๋ณด > ๋ํ๋ฏผ๊ตญ > ํฌํญ·์๋ | โ๏ธTripie
๋ํ๋ฏผ๊ตญ > ํฌํญ·์๋ ์ฌํ ์ ๋ณด โ๏ธ ๋๋ฒ ์ด vs ์์ฐจ, ํฌํญ 1๋ฐ 2์ผ ์ถ์ฒ ์ฝ์ค | ํด๋์ด ๋ช ์๋ถํฐ SNSํซํ๊น์ง ๊ฐ๋ ์์ง ์ฝ์ค โ๏ธ ํฌํญ์์ ๋ญ ํ์ง? | ํฌํญ์ด ์ฒ์์ธ ์ฌ๋๋ค์ ์ํ ์๋ด์ โ๏ธ
tripie-mauve.vercel.app
!! ์ด๋ ์ฃผ์ํ ์ ์ images ๋ฐฐ์ด์ ๋จผ์ ๋ณด์ด๊ณ ์ถ์ ํญ๋ชฉ์ ๋จผ์ ๋ฐฐ์นํด์ผ ํ๋ค. ๋ง์ฝ ์ด์ ์ ๋ฉํํ๊ทธ ์ด๋ฏธ์ง๋ค์ด ๋จผ์ ๋ณด์ด๋๋ก ํ๊ณ ์ถ๋ค๋ฉด [...previousImages, ...๋ค์์ ๋ณด์ผ ์ด๋ฏธ์ง] ์์๋ก ๋ณ๊ฒฝํด์ค์ผ ํ๋ค.
'๐ฉ๐ปโ๐ป dev' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๐ฌ React import ์ Wrapper ํจํด ํ์ฉํด๋ณด๊ธฐ (0) | 2025.03.12 |
---|---|
๐ ๊ตฟ๋ฐ์ด CRA (Sunsetting Create React App) (1) | 2025.03.09 |
[Nextjs] Dynamic metadata tag ์์ฑํ๊ธฐ (SEO ์งํ 54 โก๏ธ 91) (0) | 2025.02.25 |
Nextjs blurDataUrl dynamicํ๊ฒ ์ ๊ณตํ๊ธฐ (1) | 2025.02.15 |
polling mechanism์ผ๋ก react ref ํ์ธํ๊ธฐ (0) | 2025.01.22 |