·6 min read

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