くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt(SPA)+FirebaseでSEO!OGP!: 特定のパスだけheadだけ返すやつ

最近つくった積読ハウマッチをNuxtのSPAで作成しているけど、
シェアされたときにいい感じに画像とかを表示してほしいのでやってみた。

N番煎じ感がつよいけれど、自分の整理用〜

全体の流れ

  1. 該当のURLにアクセスがあったらリライトでFunctionを呼び出す(Hostingのrewrite)
  2. FunctionでヘッダだけのHTMLを生成。ボディには仮のパスへリダイレクト(Function)
  3. HTMLのリダイレクト先をさらにNuxt側で正しいパスリダイレクト(nuxt.config.ts)

若干複雑...

図的にはこんな感じ

f:id:wannabe-jellyfish:20190807122541p:plain

Function側のコード(index.js)

まずは、Cloud Function for Firebaseから。
リスエストのパスに応じてDBの値を取得して、OPG用のHTMLを生成。

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const db = admin.firestore();

// ********************************************************
// * Generate OPG HEAD
// ********************************************************
/**
 * OGP用のヘッダだけのHTMLを返す関数
 * @param {String} TITLE タイトル
 * @param {String} DESCRIPTION ディスクリプション
 * @param {String} OGP_URL OGP画像のURL
 * @param {String} PAGE_URL 該当ページのURL
 * @param {String} REDIRECT_URL リダイレクト先のURL
 */
const createHtml = (TITLE, DESCRIPTION, OGP_URL, PAGE_URL, REDIRECT_URL) => {
  return `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>${TITLE}</title>
    <meta property="og:title" content="${TITLE}">
    <meta property="og:image" content="${OGP_URL}">
    <meta property="og:description" content="${DESCRIPTION}">
    <meta property="og:url" content="${PAGE_URL}">
    <meta property="og:type" content="article">
    <meta property="og:site_name" content="${SITE_NAME}">
    <meta name="twitter:site" content="${BASE_URL}">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="${TITLE}">
    <meta name="twitter:image" content="${OGP_URL}">
    <meta name="twitter:description" content="${DESCRIPTION}">
  </head>
  <body>
    <script type="text/javascript">window.location="${REDIRECT_URL}";</script>
  </body>
</html>
`;
};

/**
 * '/user/<userId>'に対応するHTMLを返すFunction
 */
const BASE_URL = '<サイトのBASE_URL>'
exports.users = functions.https.onRequest(async (req, res) => {
  try {
    // PATHからパスパラメータを取得
    const [, , userId] = req.path.split("/");
    if (!userId) throw new Error(`userId is empty`);
    
    // パスパラメータを使って、DBからデータを取得
    const docRef = db.collection("users").doc(userId);
    const snap = await docRef.get();
    if (!snap.exists) throw new Error(`Not Found: userId=${userId}`);

    // DBのデータからHTML作成に必要なデータを用意
    const user = snap.data();
    const title = `${user.name}さんのページ`;
    const desc = `${user.name}さんのページの詳細です`;
    const ogpURL = "<該当ユーザのOGP画像のURL>";
    const pageURL = `${BASE_URL}/user/${userId}`;
    const redirectURL = `/_user/${userId}`;
    
    // ヘッダだけのHTMLを生成
    const html = createHtml(title, desc, ogpURL, pageURL, redirectURL);
    
    // キャッシュを設定
    res.set("Cache-Control", "public, max-age=600, s-maxage=600");
    
    // 生成したHTMLを返却
    res.status(200).end(html);
  } catch (err) {
    // エラーが発生したら'/'にリダイレクト
    console.warn(err);
    res.redirect("/");
  }
});

firebase.jsonの設定

firebase.jsonの設定。該当のパスにアクセスされたら、
リライトでFunctionを呼び出すように設定を追加。

{
  "functions": {
    "source": "functions"
  },
  "hosting": {
    "rewrites": [
      // '/user/<userId>'へのアクセスがあったらFunctionsのusersを呼び出す
      {
        "source": "/user/*",
        "function": "users"
      },
      {
        "source": "**",
        "destination": "/404.html"
      }
    ],
  },
}

nuxt.config.tsの設定

nuxt.config.jsのrouterの設定。HTML内でリダイレクトされる先を、
さらにrouter側でリダイレクト。もとに戻す感じに。

const config: NuxtConfiguration = {

  /*
   ** Router configuration
   */
  router: {
    extendRoutes(routes: NuxtRouteConfig[], resolve) {
      routes.push({
        path: "/_user/:uid",
        redirect: "/user/:uid",
        chunkNames: {}
      });
    }
  },
}

注意!! 動的パラメタのあるパスだけ使えます

ちなみに、該当のパスにHostingのHTMLがあるとダメ... Hostingの優先度がこんな感じ...

  1. 予約済み名前空間(/__*)
  2. リダイレクトの構成
  3. 正確に一致する静的コンテンツ
  4. リライトの構成

リライトよりも静的コンテンツのほうが優先度が高いので、
リライトでFunctionを呼び出されるよりも先にHTMLが返されてしまう...

nuxt generateするとHTMLが配置されてしまうので、
動的パラメタじゃないとダメかも...

以上!!

参考にしたサイト様