๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev

[Nextjs] Dynamic metadata tag ์ƒ์„ฑํ•˜๊ธฐ (Part.2)

2025.02.25 - [๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev] - [Nextjs] Dynamic metadata tag ์ƒ์„ฑํ•˜๊ธฐ (SEO ์ง€ํ‘œ 54 โžก๏ธ 91)

 

[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 ๋„ ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋กœ ๋ฎ์–ด์“ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

https://tripie-mauve.vercel.app/regions/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD/location/7bbb0a8b-63f6-4c41-811b-153c314ca0b2

 

๋„์‹œ ๋ณ„ ์—ฌํ–‰ ์ •๋ณด > ๋Œ€ํ•œ๋ฏผ๊ตญ > ํฌํ•ญ·์•ˆ๋™ | โœˆ๏ธTripie

๋Œ€ํ•œ๋ฏผ๊ตญ > ํฌํ•ญ·์•ˆ๋™ ์—ฌํ–‰ ์ •๋ณด โœ”๏ธ ๋šœ๋ฒ…์ด vs ์ž์ฐจ, ํฌํ•ญ 1๋ฐ• 2์ผ ์ถ”์ฒœ ์ฝ”์Šค | ํ•ด๋‹์ด ๋ช…์†Œ๋ถ€ํ„ฐ SNSํ•ซํ”Œ๊นŒ์ง€ ๊ฐ€๋Š” ์•Œ์งœ ์ฝ”์Šค โœ”๏ธ ํฌํ•ญ์—์„œ ๋ญ ํ•˜์ง€? | ํฌํ•ญ์ด ์ฒ˜์Œ์ธ ์‚ฌ๋žŒ๋“ค์„ ์œ„ํ•œ ์•ˆ๋‚ด์„œ โœ”๏ธ

tripie-mauve.vercel.app

 

!! ์ด๋•Œ ์ฃผ์˜ํ•  ์ ์€ images ๋ฐฐ์—ด์— ๋จผ์ € ๋ณด์ด๊ณ  ์‹ถ์€ ํ•ญ๋ชฉ์„ ๋จผ์ € ๋ฐฐ์น˜ํ•ด์•ผ ํ•œ๋‹ค. ๋งŒ์•ฝ ์ด์ „์˜ ๋ฉ”ํƒ€ํƒœ๊ทธ ์ด๋ฏธ์ง€๋“ค์ด ๋จผ์ € ๋ณด์ด๋„๋ก ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด [...previousImages, ...๋‹ค์Œ์— ๋ณด์ผ ์ด๋ฏธ์ง€] ์ˆœ์„œ๋กœ ๋ณ€๊ฒฝํ•ด์ค˜์•ผ ํ•œ๋‹ค.