인덱스 페이지 만들기
인덱스 페이지 설계부터 Hero와 Snaps 컴포넌트 구성, Nuxt SEO 모듈을 활용한 SEO 최적화까지, 블로그 작성자 소개와 검색 엔진 친화적인 설정을 통해 완성도 높은 인덱스 페이지를 만드는 방법을 알아봅니다.
index.vue
페이지 컴포넌트
인덱스 페이지 설계에 대해 오랜 시간 고민한 끝에, 블로그나 작성자를 소개하는 것이 가장 적합하다는 결론에 이르렀습니다. 이에 따라 별도의 About 페이지를 두는 대신, 인덱스 페이지가 그 역할을 수행하도록 설계했습니다.
<template>
<the-page>
<index-hero />
<hr class="my-12 md:my-14">
<index-snaps />
</the-page>
</template>
Hero 컴포넌트
히어로 영역이란, 방문자가 웹사이트를 처음 열었을 때 가장 먼저 눈에 띄는 페이지 상단의 대형 섹션을 의미합니다. 이 영역을 해당 페이지의 용도에 걸맞게 꾸며보겠습니다.
pages:
index:
title: About.
subtitle: Besfir의 블로그 튜토리얼 입니다.
description: 기존 블로그 튜토리얼들은 즉시 사용하기에는 디자인적으로 다소 부족한 점이 있었습니다. 이에 미니멀하면서도 바로 활용 가능한 수준의 블로그 튜토리얼을 구성해 보았습니다.
각 속성은 다음의 용도로 사용합니다.
title
: SEO title 속성으로 사용합니다.subtitle
: 히어로 영역 제목으로 사용합니다.description
: SEO description, 히어로 영역 description으로 사용합니다.
<template>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<div class="lg:col-span-5">
<h1 class="leading-tight text-7xl md:text-8xl font-bold text-slate-900 dark:text-slate-50 tracking-tighter">
{{ $t('pages.index.title') }}
</h1>
<p class="text-xl md:text-2xl my-5 md:my-6 font-medium">
{{ $t('pages.index.subtitle') }}
</p>
<p class="leading-relaxed text-lg my-5 md:my-6">
{{ $t('pages.index.description') }}
</p>
</div>
<nuxt-picture
class="not-prose w-full self-center lg:col-span-7"
src="/images/index/hero.jpeg"
:img-attrs="{ class: 'aspect-video rounded-xl object-cover w-full' }"
alt="profile"
width="1280"
height="720"
format="avif,webp"
sizes="sm:100vw 2xl:1536px"
density="1x 2x"
/>
</div>
</template>
Snaps 컴포넌트
블로그나 작성자를 소개하는 내용을 더욱 풍성하게 만들기 위해 6개의 사진을 배치할 수 있는 영역을 구성했습니다. 단순히 사진을 나열하는 것에서 그치지 않고, 각 사진이 SNS나 블로그의 여행 콘텐츠로 연결될 수 있도록 링크 형태로 설계했습니다.
<script setup lang="ts">
const snaps = [
{ url: 'https://www.instagram.com/', img: '/images/index/snap-1.jpg', width: 1920, height: 2400, alt: 'Thumbnail 1' },
{ url: 'https://www.instagram.com/', img: '/images/index/snap-2.jpg', width: 1920, height: 1080, alt: 'Thumbnail 2' },
{ url: 'https://www.instagram.com/', img: '/images/index/snap-3.jpg', width: 1920, height: 1273, alt: 'Thumbnail 3' },
{ url: 'https://www.instagram.com/', img: '/images/index/snap-4.jpg', width: 1920, height: 1302, alt: 'Thumbnail 4' },
{ url: 'https://www.instagram.com/', img: '/images/index/snap-5.jpg', width: 1920, height: 2560, alt: 'Thumbnail 5' },
{ url: 'https://www.instagram.com/', img: '/images/index/snap-6.jpg', width: 1920, height: 2400, alt: 'Thumbnail 6' },
]
</script>
<template>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 lg:gap-5 not-prose">
<nuxt-link
v-for="{ url, alt, width, height, img } in snaps"
:key="url"
:to="url"
class="p-1.5 rounded shadow ring-1 dark:ring-0 ring-slate-100 hover:shadow-lg dark:bg-slate-800 dark:hover:bg-primary-500"
target="_blank"
>
<nuxt-picture
:src="img"
:img-attrs="{ class: 'aspect-[3/2] rounded object-cover w-full', loading: 'lazy' }"
:width="width"
:height="height"
:alt="alt"
format="avif,webp"
sizes="sm:100vw 2xl:1536px"
density="1x 2x"
/>
</nuxt-link>
</div>
</template>
SEO
각 페이지마다 title, description, OG image 등의 SEO 설정을 수동으로 구성하려면 작업량이 많을 뿐만 아니라, SEO에 익숙치 않다면 필요한 작업을 명확히 파악하기도 어렵습니다. 이런 경우 Nuxt SEO 모듈을 활용하면 큰 도움이 됩니다.
nuxt module add @nuxtjs/seo
Nuxt SEO 모듈은 Robots, Sitemap, OG Image, Schema.org, Link Checker, 그리고 SEO Utils로 구성되어 있습니다. 각 모듈은 상당 부분이 기본값으로 자동 설정되므로, 우리는 구글 서치 콘솔에 등록하는 데 문제가 없을 정도로만 간단히 설정해보겠습니다.
사이트명
title과 site_name 메타 태그에 반영될 사이트명을 설정합니다.
export default defineNuxtConfig({
modules: [
'@nuxt/eslint',
'@nuxtjs/i18n',
'@nuxt/image',
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
'@vueuse/nuxt',
'nuxt-lodash',
'@nuxt/icon',
'@nuxtjs/seo',
],
devtools: { enabled: true },
css: ['assets/css/main.pcss'],
site: {
name: 'Besfir',
},
compatibilityDate: '2024-11-01',
eslint: {
config: {
stylistic: true,
},
},
i18n: {
baseUrl: 'https://test.io',
locales: [
{ code: 'ko', name: '한국어', file: 'ko.yaml', language: 'ko-KR' },
{ code: 'en', name: 'English', file: 'en.yaml', language: 'en-US' },
],
strategy: 'prefix_except_default',
defaultLocale: 'en',
detectBrowserLanguage: {
alwaysRedirect: true,
fallbackLocale: 'en',
},
vueI18n: './i18n/i18n.config.ts',
},
icon: {
size: '20',
customCollections: [{
prefix: 'besfir',
dir: 'assets/icons/',
}],
},
})
OG Image
OG Image 모듈에서 사용하는 렌더러인 Satori와 Chromium 브라우저는 영어 이외의 언어를 제대로 렌더링하지 못합니다. 이를 해결하기 위해 Noto Sans Korean 폰트를 설치하고 사용하도록 설정하겠습니다.
export default defineNuxtConfig({
modules: [
'@nuxt/eslint',
'@nuxtjs/i18n',
'@nuxt/image',
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
'@vueuse/nuxt',
'nuxt-lodash',
'@nuxt/icon',
'@nuxtjs/seo',
],
devtools: { enabled: true },
css: ['assets/css/main.pcss'],
site: {
name: 'Besfir',
},
compatibilityDate: '2024-11-01',
eslint: {
config: {
stylistic: true,
},
},
i18n: {
baseUrl: 'https://test.io',
locales: [
{ code: 'ko', name: '한국어', file: 'ko.yaml', language: 'ko-KR' },
{ code: 'en', name: 'English', file: 'en.yaml', language: 'en-US' },
],
strategy: 'prefix_except_default',
defaultLocale: 'en',
detectBrowserLanguage: {
alwaysRedirect: true,
fallbackLocale: 'en',
},
vueI18n: './i18n/i18n.config.ts',
},
icon: {
size: '20',
customCollections: [{
prefix: 'besfir',
dir: 'assets/icons/',
}],
},
ogImage: {
fonts: ['Noto Sans KR'],
},
})
모든 페이지에서 OG Image를 사용할 수 있도록 app.vue
컴포넌트에서 defineOgImageComponent()
컴포저블을 호출합니다. 저는 모든 페이지에 공통적으로 사용할 헤드라인을 설정하고자 headline
props를 전달했습니다. 이렇게 설정하면 모듈에서 각 페이지의 title
과 description
메타 태그를 자동으로 읽어와 OG Image에 반영됩니다.
<script setup lang="ts">
const head = useLocaleHead()
defineOgImageComponent('Nuxt', {
headline: 'besfir',
})
</script>
<template>
<Html
:lang="head.htmlAttrs?.lang"
:dir="head.htmlAttrs?.dir"
>
<Head>
<template
v-for="link in head.link"
:key="link.id"
>
<Link
:id="link.id"
:rel="link.rel"
:href="link.href"
:hreflang="link.hreflang"
/>
</template>
<template
v-for="meta in head.meta"
:key="meta.id"
>
<Meta
:id="meta.id"
:property="meta.property"
:content="meta.content"
/>
</template>
</Head>
<Body>
<nuxt-loading-indicator />
<the-header />
<nuxt-page />
<the-footer />
</Body>
</Html>
</template>
메타 태그
useSeoMeta()
컴포저블에 description
을 props로 전달하여 메타 태그에 반영합니다. 인덱스 페이지에서는 사이트명만 제목으로 지정하고 싶기 때문에, title
속성은 설정하지 않겠습니다.
<script setup lang="ts">
const { t } = useI18n()
useSeoMeta({
// title: t('pages.index.title'),
description: t('pages.index.description'),
})
</script>
<template>
<the-page
class="prose-h1:text-7xl md:prose-h1:text-8xl prose-h1:leading-[1.1] prose-h1:mb-3.5 prose-h1:font-bold"
is-prose
>
<index-hero />
<hr>
<index-snaps />
</the-page>
</template>
여기까지 프로젝트 구조
소스 코드의 파일 경로는 이 섹션을 참고해주세요
.
├── README.md
├── app.vue
├── assets
│ ├── css
│ │ └── main.pcss
│ └── icons
│ └── logo.svg
├── bun.lockb
├── components
│ ├── App
│ │ ├── Button.vue
│ │ ├── Link.vue
│ │ └── SnsLinks.vue
│ ├── Index
│ │ ├── Hero.vue
│ │ └── Snaps.vue
│ └── The
│ ├── Footer.vue
│ ├── Header.vue
│ ├── LangSwitcher.vue
│ ├── Page.vue
│ └── ThemeSwitcher.vue
├── constant
│ └── ButtonStyle.ts
├── eslint.config.mjs
├── i18n
│ ├── i18n.config.ts
│ └── locales
│ ├── en.yaml
│ └── ko.yaml
├── nuxt.config.ts
├── package.json
├── pages
│ ├── archives.vue
│ ├── blog.vue
│ └── index.vue
├── public
│ ├── favicon.ico
│ ├── images
│ │ └── index
│ │ ├── hero.jpeg
│ │ ├── snap-1.jpg
│ │ ├── snap-2.jpg
│ │ ├── snap-3.jpg
│ │ ├── snap-4.jpg
│ │ ├── snap-5.jpg
│ │ └── snap-6.jpg
│ └── robots.txt
├── server
│ └── tsconfig.json
├── tailwind.config.ts
├── tsconfig.json
└── types
└── Button.ts