本記事のゴール

なぜかメタタグがうまく読み込まれないNext.jsプロジェクトの原因を調査したら 原因はわかりやすかった。

作業

問題に行き着いた経緯

このブログをFacebookのシェアデバッガーで確認したところ、

同ドメインで、ルートに設置してあるコーポレートページのOGPが表示されて 困っていました。

// シェアデバッガーでは、以下のように表示される
og:url	   https://moldspoon.jp/ // <= https://moldspoon.jp/blog としたい
og:title   MoldSpoon Inc. // <= MoldSpoon Inc. blog としたい

そこでChromeの 「要素を検証」 でソースコードを確認しながら、何が原因でタイトルが誤っているのかを 確認。

ただ、「要素を検証」でソースコードを閲覧した時点では正しいメタタグが表示されてるみたいで...困りました。

view:https://moldspoon.jp/blog
<meta property="og:url" content="moldspoon.jp/blog">
<meta property="og:title" content="Moldspoon Inc. blog">

X(Twitter)は別途 twitter:xxxでTwitterカードのメタタグを設定しますが、同じようにポスト時に誤ったOGP情報が表示されてしまっています。

Next.jsの特性から考え直した。

すると、シンプルな原因が...見えてきました。

next/headはページのエンドポイントとなる.tsxファイルだけではなく、配下のcomponentや拡張したレイアウトファイルなどから簡単にメタタグへの処理の記入ができる便利なComponentです。

こちらを使って私も当ブログのメタタグを定義していますが、ページの判定はnext/routerを使って動的に行っています。そのため、next/routerの判定が終わっていない時点では、仮のURLやタイトルが入ってしまっており、トップページのタイトルなどが適用されてしまっていた ようです。

具体的には以下のようになっていました。(Next.js 12で実装しています。Next13でも同じように動くと思います)

before
components/Head/CommonHead.tsx
import { ReactElement } from 'react'
import Head from 'next/head'

type Props = {
  head?: ReactElement
  headTitle?: string
  headKeyword?: string
  headDescription?: string
  headOgpPath?: string
  currentUrl?: string
  canonicalUrl?: string
}

const defaultHeadTitle = "MoldSpoon Inc."
const defaultKeyword = "MoldSpoon,Keyword,Web Service"
const defaultHeadDescription = "このように楽しく、役に立つWebサービスを愚直に作っています。"
const defaultHeadOgpPath = "/img/ogp.png"
const defaultCurrentUrl = process.env.baseUrl

const CommonHead: React.FC<Props> = ({ head,
  headTitle,
  headKeyword,
  headDescription,
  headOgpPath,
  currentUrl,
  canonicalUrl }) => {
  // ... 本筋と関係ない処理が色々されていたが、それは略 ...
  return (
    <Head>
      <title>{headTitle ? headTitle : defaultHeadTitle}</title>
      <meta name="google" content="notranslate" />
      <meta name="referrer" content="strict-origin" />
      <meta name="description" content={headDescription ? headDescription : defaultHeadDescription} />
      <meta name="keyword" content={headKeyword ? headKeyword : defaultKeyword} />
      <meta name="generator" content="yuku_tas" />
      <meta name="author" content="MoldSpoon Inc." />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta property="og:site_name" content="MoldSpoon Inc." />
      <meta property="og:type" content="website" />
      <meta property="og:title" content={headTitle ? headTitle : defaultHeadTitle} />
      <meta property="og:url" content={currentUrl ? currentUrl : defaultCurrentUrl} />
      <meta property="og:site_name" content="MoldSpoon Inc." />
      <meta property="og:description"
        content={headDescription ? headDescription : defaultHeadDescription} />
      <meta property="og:image" content={process.env.baseUrl + (headOgpPath ? headOgpPath : defaultHeadOgpPath)} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:creator" content={`@yuku_tas`} />
      <meta property="twitter:image" content={process.env.baseUrl + (headOgpPath ? headOgpPath : defaultHeadOgpPath)} />
      <link rel="icon" href="/favicon.ico" />
      <link rel="canonical" href={canonicalUrl ? canonicalUrl : currentUrl ? currentUrl : defaultCurrentUrl}></link>
    </Head>
  )
}
export default CommonHead

このような書き方では、主にnext/routerに依存するcurrentUrlを判定に使っている時点で、

  1. 三項演算子により、まずトップページの内容のHeadタグが表示されてしまう(ページを読み込んだ時点では100%、falseの分岐の方に行き、トップページのものが表示されてしまう。)
  2. FBやTWのスクレイパは動的に吐き出されたメタタグのうち、最初に表示されたものを読み込みそれ以上の判定をしない

ということになってしまうようでした。ということで改修を行い以下のようにしました。

after
components/Head/CommonHead.tsx
import { ReactElement } from 'react'
import Head from 'next/head'

type Props = {
  head?: ReactElement
  headTitle?: string
  headKeyword?: string
  headDescription?: string
  headOgpPath?: string
  currentUrl?: string
  canonicalUrl?: string
}

const defaultHeadTitle = "MoldSpoon Inc."
const defaultKeyword = "MoldSpoon,Keyword,Web Service"
const defaultHeadDescription = "このように楽しく、役に立つWebサービスを愚直に作っています。"
const defaultHeadOgpPath = "/img/ogp.png"
const defaultCurrentUrl = process.env.baseUrl

const CommonHead: React.FC<Props> = ({ head,
  headTitle,
  headKeyword,
  headDescription,
  headOgpPath,
  currentUrl,
  canonicalUrl }) => {
  if (currentUrl == null) {
    return
  }

  return (
    <Head>
      <title>{headTitle ?? defaultHeadTitle}</title>
      <meta name="google" content="notranslate" />
      <meta name="referrer" content="strict-origin" />
      <meta name="description" content={headDescription ?? defaultHeadDescription} />
      <meta name="keyword" content={headKeyword ?? defaultKeyword} />
      <meta name="generator" content="yuku_tas" />
      <meta name="author" content="MoldSpoon Inc." />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta property="og:site_name" content="MoldSpoon Inc." />
      <meta property="og:type" content="website" />
      <meta property="og:title" content={headTitle ?? defaultHeadTitle} />
      <meta property="og:url" content={currentUrl} />
      <meta property="og:site_name" content="MoldSpoon Inc." />
      <meta property="og:description" content={headDescription ?? defaultHeadDescription} />
      <meta property="og:image" content={process.env.baseUrl + (headOgpPath ?? defaultHeadOgpPath)} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:creator" content={`@yuku_tas`} />
      <meta property="twitter:image" content={process.env.baseUrl + (headOgpPath ?? defaultHeadOgpPath)} />
      <link rel="icon" href="/favicon.ico" />
      <link rel="canonical" href={canonicalUrl ?? currentUrl}></link>
    </Head>
  )
}
export default CommonHead

以下を行なっています。

  1. currentUrlがundefinedの時は、Head自体を返さず空の結果を返し、currentUrlが取れるまでスクレイパを待たせる
  2. 三項演算子は書き方として冗長だったので、NULL合体演算子(??)を使う。

これでOpenGraphの読み込みが確認できたので、改修を終えたと判断しました。

ただ、動的にHeadタグを生成しスクレイパを待たせているので、あとでSpeed Testなどを行いパフォーマンスに問題がありそうな場合は別の実装方法も検討したいと思います。(その場合、next/linkを使うのをやめないといけないですが、多分そうはならないでしょう。Next公式のビルトインされている、信用度が高いライブラリですからね。)

まとめ

今回はたまたま、

と全く役割が違うアプリケーションがサブディレクトリにあったため、この挙動に気付きやすかったです。


サブディレクトリ以下の役割がトップページなどデフォルトのメタタグの挙動と全く一緒だと、

など確認が必要な箇所が一気に広がり、ハマり込みそうな危険な問題だなと思いました。


そのため、この記事が何かのお役に立てれば幸いだと思いました。
最後までお読みいただきありがとうございました!