Nuxt Content v2とNuxt3を利用したブログのSEO対策ガイド

2023-09-29
2023-09-29

markdownで記事を書いてデプロイするだけがブログではないです。どうせならSEO対策もちゃんとやりたいです。今回はNuxt Content v2を利用したブログにおけるSEO対策について紹介します。

前回記事

ブログ作成にあたり技術的にやること

ブログ構築にあたり技術的に行う項目をいくつかまとめました。

  • タイトルタグやメタディスクリプションを適切に設定する
  • canonicalの設定
  • OGPの設定
  • Googleアナリティクスの設定
  • サイトマップの作成
  • 構造化データの定義
  • 画像の最適化

このほかにもありそうですが、大体この辺りを押さえておけば大丈夫なはずです。

Nuxt3およびNuxt Content v2で基本的なheadタグに書く情報を設定する方法

一般的なWebページでは、以下のようにheadタグ内にページのタイトルや詳細を記載します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ここにページのタイトル</title>
    <meta name="description" content="ここにページの詳細">
</head>

Nuxt3ではこういったheadタグの情報はnuxt.config.tsにhead()という記述ができ、そこにメタタグやリンクタグの情報を書けます。

ただし、nuxt.config.tsに記載する情報はページ間で共通の情報のみを書きます。

export default defineNuxtConfig({
    ...,
    app: {
        head: {
        htmlAttrs: { lang: 'ja', prefix: 'og: <https://ogp.me/ns#>' },
        meta: [
            { charset: 'utf-8' },
            { name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'twitter:card', content: 'summary_large_image' },
        { name: 'twitter:site', content: '@oio_blog' }
        ]
        }
    }
})

次にページごとに動的に変わる値はどう設定するのか説明します。

Nuxt3ではheadタグの情報変更に便利なコンポーザブル(Composable)が2つ用意されています。

二つの違いはざっくりとuseHeadの方がheadに書く情報を細かく書けること、useSeoMetaの方は一般的なSEO対策で必要なことが簡潔かつTypeScriptのサポートによってタイポが少なく書けることでしょうか。

この記事の冒頭で挙げたようなよくあるheadの記載事項はuseSeoMeta()で実現できます。

Nuxt Content v2でのheadサポート

Nuxt Content v2でも一般的なheadの記載事項についての機能があります。

まず、useContentHead()というコンポーザブルを使う方法があります。これはドキュメントにあるようにqueryContentからの戻り値を入力すると、markdownファイルのフロントマターを参照し、titleやdescriptionをheadに書いてくれます。

<script setup lang="ts">
const { data: page } = await useAsyncData('my-page', queryContent('/').findOne)
useContentHead(page)
</script>

さらにNuxt Content v2はデフォルトで自動的にmarkdownのフロントマターからheadの情報を生成してくれます。

ただし、この機能で必要なheadの情報を揃えようとするとフロントマターの記載事項がすごく増えます。

個人的には画像等の設定はプログラム的に書いたほうが楽なので今回はhead自動生成はオフにしてます。

オフにする方法は以下のようにnuxt.config.tsに設定するだけです。

export default defineNuxtConfig({
  content: {
    contentHead: false
  }
})

OGP+SNS (X 向け) の設定

OGP (Open Graph Protocol) と個別のSNS設定についてです。SNSと言っても今回はX(旧Twitter)についてのみです。

この設定を適切に行うことで、SNS上でのユーザーに見やすいカード表示が可能となり、ブログへの流入へつながる可能性が高まります。

設定は先ほどのNuxt3のuseSeoMeta()を使います。以下を[…slug].vueに書きます。

<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
const appConfig = useAppConfig()
const route = useRoute()
const { data: article } = await useAsyncData(() => {
  return queryContent().where({ _path: { $eq: withoutTrailingSlash(route.path) } }).findOne()
})
useSeoMeta(
  {
    title: article?.value?.title,
    description: article?.value?.description || article?.value?.title,
    ogTitle: article?.value?.title,
    ogDescription: article?.value?.description || article?.value?.title,
    ogUrl: `${appConfig.siteMetadata.siteUrl}/img/twitter-card.png`,
    ogImage: `${appConfig.siteMetadata.siteUrl}/img/twitter-card.png`,
    twitterTitle: article?.value?.title,
    twitterDescription: article?.value?.description || article?.value?.title,
    twitterImage: `${appConfig.siteMetadata.siteUrl}${article?.value?.thumbnail}`,
    robots: 'follow, index'
  }
)
</script>

useAppConfig()app.config.tsという名前のファイル内容を読み取ってくれます。

参考までにこのブログで使っているapp.config.tsです。

export default defineAppConfig({
    socials: {
        twitter: 'https://twitter.com/oio_blog',
        github: ''
    },
    siteMetadata: {
        title: '学びノート',
        author: 'おい丸',
        headerTitle: '学びノート',
        description: 'IT企業に務めるサラリーマンの学びの記録です',
        language: 'ja-jp',
        siteUrl: 'https://www.oio-blog.com',
        siteAbout: 'https://www.oio-blog.com/about',
        siteRepo: 'https://github.com',
        twitter: 'https://twitter.com/oio_blog',
        analytics: {
            simpleAnalytics: false, // true or false
            googleAnalyticsId: 'UA-XXXXXXX-1', // e.g. UA-000000-2 or G-XXXXXXX
        },
        perPage: 10,
        tagPerPage: 30
    }
})

ハマったところ

queryContent()利用時にawaitをつけ忘れたことにより、ブラウザの開発者ツールから見たときはheadに必要なOGPの設定ができているのに実際にはカードが表示されない現象に遭遇しました。

awaitをつけ忘れると記事情報をjsで取得が終わる前に読み取りが行われてしまうからの模様。

画像最適化

SEOおよびユーザビリティの観点から、画像の最適化は必須であり、表示の速度や見た目に配慮することが重要です。

NuxtにはNuxt imageがあり、これを使うとビルド時に画像を簡単に最適化してくれます。

imgタグについてはNuxt Content v2の機能でマークダウンからHTMLにレンダリングする際のものを指定できます。以下のvueファイルをcomponents/content/ProseImg.vueというパスとファイル名で作成します。

<script setup lang="ts">
const props = defineProps({
    imgSrc: {
        type: String,
        default: ''
    },
    alt: {
        type: String,
        default: ''
    },
    width: {
        type: [String, Number],
        default: undefined
    },
    height: {
        type: [String, Number],
        default: undefined
    }
})
</script>
<template>
    <nuxt-img class="mx-auto h-auto w-fit" quality="100" sizes="sm:100vw md:50vw lg:1024px" format="webp" loading="lazy"
        :src="props.imgSrc" :alt="props.alt" :width="width" :height="height" />
</template>

このようにすることでマークダウンファイル内で画像を表示する記法![]()を使用した際に上記のコンポーネントが利用されます。(cssのclassは適宜変更してください)

yarn generate時にwebp変換とレスポンシブ用にリサイズした画像を生成してくれます。

ちなみに生成されたファイルは.webp拡張子ではなく.pngになっている現象がありますが、minetypeを調べると.webp形式になっているようです。

参考:WebP image have .png extension ?

サイトマップの作成

サイトマップの作成は、公式のSitemapを参照してください。この方法を使うとyarn generate使用時にsitemap.ymlが生成されます。

公式サイト通りなのでここでは割愛

Google アナリティクス(GA4)

nuxt-gtagというモジュールを使用します。これを使うと簡単にGAと連携できます。

nuxt.config.ts に以下のようにidを書くだけです。

export default defineNuxtConfig({
  modules: ['nuxt-gtag'],
  gtag: {
    id: 'G-XXXXXXXXXX',
        loadingStrategy: 'async'
  }
})

loadingStrategy はおまかせしますが、私の場合asyncにした方がサイトパフォーマンスが良かったです。デフォルトはdeferになってます。

Googleの構造化データ

これはやらなくてもいいかもですが、一応Google の構造化データも生成しておきます。

nuxt-jsonldという便利なパッケージがあるのでこれをインストールします。

yarn add -D nuxt-jsonld

こちらもコンポーザブルとして利用可能です。以下のように書きます。

useJsonld({
    '@context': 'https://schema.org',
    '@type': 'Article',
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': currentPath.value,
    },
    name: 'static json',
    headline: article.value.title,
    image: {
      '@type': 'ImageObject',
      url: `${appConfig.siteMetadata.siteUrl}${article.value.thumbnail}`,
    },
    datePublished: article.value.createdAt,
    dateModified: article.value.updatedAt,
    author: [{
      '@type': 'Person',
      name: appConfig.siteMetadata.author,
      url: appConfig.siteMetadata.siteAbout
    }],
    description: article.value.description,
  });

長くなるので変数定義の部分は割愛しました。記事の前半部分のコードを見れば大体想像できると思います。

PageSpeed Insightsの結果

ここまででの画像最適化、GAの設定、その他head情報記載で画像を表示したページでも97-98点くらいの点数がPageSpeed Insightsで出ます。

ここに自分好みのブログの装飾を色々加えると点数は下がるのですが、それでも90~95点はでます。

以下は目次やら作成日時用のアイコンを追加した際の結果です。

PageSpeed Insights

ただどうやら文章量が増えるとややスコアが落ち込みます。それでも80点台です。

パフォーマンスを下げない工夫

PageSpeed Insightsでいろいろ試してパフォーマンスに影響がでる実装を調査しました。

今の所の検証結果の結論としてはv-ifをなるべく使わないことが大事です。

またv-ifを減らす以外にも子コンポーネントに切り出すも効果がありそうでした。

ただ一番は圧倒的にv-ifです。

v-ifを回避した実装をしようとすると自然と子コンポーネントに切り出すことが増える気がします。

まとめ

この記事では、Nuxt Content v2を利用したブログのSEO対策について詳細に説明しました。主に、タイトルタグやメタディスクリプションの適切な設定、canonicalの設定、OGPの設定、Googleアナリティクスの設定、サイトマップの作成、構造化データの定義、画像の最適化について触れてきました。

特に、Nuxt Content v2とNuxt3でのSEO最適化の基本的な設定方法や、v-ifの使用を避けることの重要性について解説しました。また、Page Speed Insightでのパフォーマンス評価においても、適切な最適化を行うことで97-98点のスコアを達成できることが示されました。