😤

【もっと!早く知りたかった】Next.js(SSR)の際にfirestoreに紐づくsitemap.xmlの作り方

2022/09/06に公開

Next.jsにてsitemap.xmlを作る方法はいくつかあります。SSRなのかどうかでやり方は変わってきますが、よく話題になっているのは next-sitemapだと思います。
https://www.npmjs.com/package/next-sitemap

CGMとかUGCとかページ数が莫大(動的ページが大きな役割を持つ系のサービス)であると、コンテンツのワードなりでロングテールを拾いたい願望から、sitemap.xmlの意識も高まると思います。
今回はそんな、コンテンツ型のプロダクトをつくっている中でデータベースにfirestoreを使ってしまったパターンのtipsをお話ししていきたいと思います。

SSRをする時は、自作のcomponentでxmlファイルを生成する

僕が実際にやる上で参考にした記事は以下です。
https://gladevise.com/next-sitemap-examples

今回やるfirestoreを使ったsitemap.xml作成の肝になる部分は、1documentの中に色々とぶち込んでいく点と考えてます
1documentのMaxサイズは Max document size: 1 MiB (1,048,576 bytes)とのことなので、結構な数が入ることになります。
1documentで入り切らないようになった時(贅沢な悩み)の対策はまた別の工夫が必要そうですが、一旦はこれで...
ぶちこむとは具体的にはfirestoreのarrayUnionなどを使うことで、documentに紐づいているarrayの中身を取りにいくということです。

ステップ1 Sitemapのモデルを準備する

interface Sitemap {
  path: string
  lastmod?: string
  changefreq: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly'
  priority: number
}
export type { Sitemap }

特に難しいことはありません。

ステップ2 静的ファイルのモデルを準備する

const StaticFields: Sitemap[] = [
  {
    path: '',
    lastmod: new Date().toISOString(),
    changefreq: 'hourly',
    priority: 1.0,
  },
  {
    path: '/contact',
    changefreq: 'monthly',
    priority: 0.5,
  },
]

ステップ3 APIRoutesでfirestoreから一覧をsitemapを生成するのに必要なパス一覧を取得するAPIを作る

const Api = async (req: NextApiRequest, res: NextApiResponse<any>) => {
  switch (req.method) {
    case 'GET':
      try {
        const _ref = doc(firestore, 'sitemap', 'users')
        const _snap = await getDoc(_ref)
        const _list = _snap.data()

	// ここでキャッシュをいれるなどする
	res.status(200).json({
          users: _list.users,
        })
        return
      } catch {
        res.status(400))
      }
      break
    case 'POST':
      res.status(400)
      break
  }
}

ステップ4 firebase-functionsにある、firestoreのトリガーでidを集めるようにする

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { getFirestore, FieldValue } from 'firebase-admin/firestore'

admin.initializeApp()
const firestore = getFirestore()

...

/**
 * userが追加された時に
 */
exports.addUser = functions.firestore.document('users/{userId}').onCreate((snap, context) => {
  const _id = snap.data().id
  if (_id) {
    firestore.doc('sitemap/users').update({
      paths: FieldValue.arrayUnion(_id),
    })
  }
})

/**
 * userが削除された時に
 */
exports.removeUser = functions.firestore.document('users/{userId}').onDelete((snap, context) => {
  const _id = snap.data().id
  if (_id) {
    firestore.doc('sitemap/users').update({
      paths: FieldValue.arrayRemove(_id),
    })
  }
})

ステップ5 xmlファイルを作る関数を準備する

// 以下URLを参考にした
// https://gladevise.com/next-sitemap-examples
import { Sitemap } from './model'
import { StaticFields } from './static'

const generateSitemapXml = async () => {
  const _staticFields = StaticFields

  const _url = 'https://.../api/sitemap/user'
  const _resp = await fetch(_url)
  const _data = await _resp.json()
  const _paths = _data.paths as string[]
  const _userFields: Sitemap[] = _paths.map((id: string) => ({
    path: `user/${id}`,
    changefreq: 'weekly',
    priority: 0.7,
  }))

  const fields = _staticFields.concat(_userFields)

  let xml = `<?xml version="1.0" encoding="UTF-8"?>`
  xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`

  fields.forEach(post => {
    const url = new URL(post.path, `https://${process.env.MYDOMAIN}`)
    xml += `
      <url>
        <loc>${url.toString().replace(/\/$/, '')}</loc>
        <lastmod>${post.lastmod}</lastmod>
        <changefreq>${post.changefreq}</changefreq>
        <priority>${post.priority}</priority>
      </url>
    `
  })

  xml += `</urlset>`

  return xml
}

export default generateSitemapXml

ステップ6 xmlファイルを返す/pages/sitemap.xml.tsxをつくる

import { GetServerSideProps } from 'next'
import generateSitemapXml from 'src/framework/sitemap/sitemap'

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const xml = await generateSitemapXml()

  res.statusCode = 200

  res.setHeader('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate')
  res.setHeader('Content-Type', 'text/xml')
  res.end(xml)

  return {
    props: {},
  }
}

const Sitemap = (): void => {
  return
}

export default Sitemap

ちょっと大変だけど、作れるのは作れるみたいで安心ですね!

Discussion