Building the Index Page
Learn how to create a polished index page introducing the author, with a hero and snaps section, plus SEO optimization using the Nuxt SEO module.
index.vue
Page Component
After much consideration, I concluded that the index page should focus on introducing the blog or its author. Therefore, instead of creating a separate About page, I designed the index page to fulfill that purpose.
<template>
<the-page>
<index-hero />
<hr class="my-12 md:my-14">
<index-snaps />
</the-page>
</template>
Hero Component
The hero section is the prominent area at the top of a webpage, immediately visible when a visitor opens the site. Let’s design this section to align with the purpose of the page.
pages:
index:
title: About.
subtitle: This is Besfir's blog tutorial.
description: Existing blog tutorials often fell short in terms of aesthetics for immediate use. To address this, I’ve created a tutorial that is minimalistic yet fully ready for use as a blog.
Each property is used for the following purposes:
title
: Used as the SEO title attribute.subtitle
: Used as the hero section title.description
: Used as the SEO description and the hero section 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 Component
To enrich the content introducing the blog or author, I created an area for six photos. Rather than simply displaying images, each photo is linked to related content, such as travel posts on social media or the blog, enabling seamless navigation.
<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
Manually configuring SEO settings like title
, description
, and OG images for every page can be time-consuming and challenging, especially if you're unfamiliar with SEO practices. In such cases, the Nuxt SEO module can be a tremendous help.
nuxt module add @nuxtjs/seo
The Nuxt SEO module includes features like Robots, Sitemap, OG Image, Schema.org, Link Checker, and SEO Utils. Many of these settings are automatically configured by default. We'll focus on basic settings sufficient for registering the site with Google Search Console.
Site Name
Set the site name, which will be reflected in the title
and site_name
meta tags.
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
The renderer used by the OG Image module, Satori and the Chromium browser, does not support rendering non-English languages properly. To address this, we’ll install and configure the Noto Sans Korean font.
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'],
},
})
To ensure OG Images are available on all pages, call the defineOgImageComponent()
composable in the app.vue
component. I passed a headline
prop to set a common headline for all pages. With this setup, the module automatically reads the title
and description
meta tags of each page and incorporates them into the 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>
Meta Tags
Use the useSeoMeta()
composable to pass the description
as a prop and reflect it in the meta tags. For the index page, I want the site name to serve as the title, so I won’t specify the title
property.
<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>
Project Structure So Far
Refer to this section for the file paths of the source code.
.
├── 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