多言語Gatsby.jsサイトの、XMLサイトマップ作成方法
当サイトroute360.devは、Gatsby.jsで作られた多言語サイトです。
今回、Gatsby.js公式のサイトマップ生成プラグイン「gatsby-plugin-sitemap」を用いて、多言語サイト用にカスタマイズする方法を説明します。
このブログはリポジトリを公開していますので、詳細なコードを確認されたい方はアクセスしてみてください。この記事のコードは、説明のため多少簡略化しています。
動作環境:
- gatsby v5.10.0
- gatsby-plugin-sitemap v6.10.0
- react v18.2.0
- node v18.16.0
前提条件
今回は当サイトの例として、Markdownでコンテンツが構成されるサイトで説明します。CMSを使っている場合は、query
部分を適宜修正してください。
ページのURLパスの構成
ページのURLパスの構成は以下の通りです。
- 記事ページ
/[lang]/post/[slug]/
- 独立ページ
/[lang]/[slug]/
※about・contactページ等 - タグページ
/[lang]/tag/[slug]/
- タグページ(2ページ目以降)
/[lang]/tag/[slug]/page/[num]/
- トップページ
/[lang]/
- トップページ(2ページ目以降)
/[lang]/page/[num]/
ポイント:
- 翻訳記事のスラッグはすべての言語で共通
- 必ず言語コードがルートドメイン直下のパスに入る
翻訳記事のスラッグが言語毎に違う場合や、デフォルト言語で言語コードをパスに含めない場合、コードが多少違ってきますので、コピペでのご利用にご注意ください。
また、今回は言語毎にアーカイブページのページ数が違う場合も考慮します。
目標
Googleのガイドに書いてあるようなサイトマップ生成を目指します。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://www.example.com/english/page.html</loc>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://www.example.de/deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="de-ch"
href="https://www.example.de/schweiz-deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://www.example.com/english/page.html"/>
</url>
<url>
<loc>https://www.example.de/deutsch/page.html</loc>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://www.example.de/deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="de-ch"
href="https://www.example.de/schweiz-deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://www.example.com/english/page.html"/>
</url>
<url>
<loc>https://www.example.de/schweiz-deutsch/page.html</loc>
<xhtml:link
rel="alternate"
hreflang="de"
href="https://www.example.de/deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="de-ch"
href="https://www.example.de/schweiz-deutsch/page.html"/>
<xhtml:link
rel="alternate"
hreflang="en"
href="https://www.example.com/english/page.html"/>
</url>
</urlset>
コード概要
早速ですが、コードです。
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-sitemap",
options: {
query: `
{
site {
siteMetadata {
siteUrl
}
}
allSitePage {
nodes {
path
}
}
}`,
resolvePages: ({ allSitePage: { nodes: allPages } }) => {
const pages = allPages.map(page => {
const alternateLangs = allPages
.filter(
alterPage =>
alterPage.path.replace(/\/.*?\//, "/") ===
page.path.replace(/\/.*?\//, "/")
)
.map(alterPage => alterPage.path.match(/^\/([a-z]{2})\//))
.filter(match => match)
.map(match => match[1])
return {
...page,
...{ alternateLangs },
}
})
return pages
},
serialize: ({ path, alternateLangs }) => {
const pagepath = path.replace(/\/.*?\//, "/")
const xhtmlLinks =
alternateLangs.length > 1 &&
alternateLangs.map(lang => ({
rel: "alternate",
hreflang: lang,
url: `/${lang}${pagepath}`,
}))
let entry = {
url: path,
changefreq: "daily",
priority: 0.7,
}
if (xhtmlLinks) {
entry.links = xhtmlLinks
}
return entry
},
},
},
],
}
※当ページの実際のコードは投稿ページのlastmod
も追加しているため、もう少し複雑になっています。
サイトマップの出力例
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
>
<url>
<loc>https://route360.dev/en/post/gatsby-i18n/</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://route360.dev/en/post/gatsby-i18n/" />
<xhtml:link rel="alternate" hreflang="fr" href="https://route360.dev/fr/post/gatsby-i18n/" />
<xhtml:link rel="alternate" hreflang="ja" href="https://route360.dev/ja/post/gatsby-i18n/" />
</url>
<url>
<loc>https://route360.dev/en/post/codeium/</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://route360.dev/en/post/codeium/" />
<xhtml:link rel="alternate" hreflang="fr" href="https://route360.dev/fr/post/codeium/" />
<xhtml:link rel="alternate" hreflang="ja" href="https://route360.dev/ja/post/codeium/" />
</url>
<!-- 以下略 -->
</urlset>
参考までに、当サイトのサイトマップはこちら。
コードの説明
概要としては、
- すべてのページのパスを展開し、同じURLパスの翻訳ページの言語コード(そのURL自体を含む)を配列
alternateLangs
へ代入 alternateLangs
(言語数)が2以上の場合、<xhtml:link rel="alternate" hreflang="lang_code" href="page_path" />
をサイトマップに追加する
と言った具合です。
各URLパスの翻訳ページの言語コードを配列にする
まずは前半部分。
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-sitemap",
options: {
// ...
resolvePages: ({ allSitePage: { nodes: allPages } }) => {
const pages = allPages.map(page => {
const alternateLangs = allPages
// 同じパスの翻訳ページ(そのURL自体を含む)を抽出
// 例)/en/first-post/ と /ja/first-post/ の /first-post/ が一致すればtrue
.filter(
alterPage =>
alterPage.path.replace(/\/.*?\//, "/") ===
page.path.replace(/\/.*?\//, "/")
)
// 翻訳ページのパスから言語コードを取得し配列化
.map(alterPage => alterPage.path.match(/^\/([a-z]{2})\//))
// nullを排除 .filter(Boolean)でも可
.filter(match => match)
// 言語コードのみを配列化
.map(match => match[1])
return {
...page,
...{ alternateLangs }, // 言語コードをデータに追加
}
})
return pages
},
// ...
},
},
],
}
正規表現でパスから以下の文字列を取得する、若干の荒技?です。
- 言語コード
- 言語コードを除いたページパス
当サイトの場合、gatsby-node.js
でpageContextに言語コード・markdownRemarkにスラッグを渡しているので、クエリから現在の言語コードやスラッグも取得できますが、説明と汎用性のためにこのようなコードにしました。
CMSを使っている場合はクエリから言語コードを取得できる場合もあると思います。
言語数が2以上の場合のみ、xhtml:linkを追加
次に、後半部分です。
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-sitemap",
options: {
//...
serialize: ({ path, alternateLangs }) => {
// 言語コードを除いたページパスを取得
const pagepath = path.replace(/\/.*?\//, "/")
// 翻訳ページ(そのURL自体を含む)用コードを生成
const xhtmlLinks =
alternateLangs.length > 1 && // 翻訳数が2以上の場合
alternateLangs.map(lang => ({
rel: "alternate",
hreflang: lang,
url: `/${lang}${pagepath}`,
}))
// デフォルトの<url>要素
let entry = {
url: path,
changefreq: "daily",
priority: 0.7,
}
// 翻訳がある場合は<url>に子要素<xhtml:link rel="alternate" hreflang="lang">を追加
if (xhtmlLinks) {
entry.links = xhtmlLinks
}
return entry
},
},
},
],
}
これにより、翻訳ページがある場合にのみ、<url>
の子要素<xhtml:link rel="alternate" hreflang="lang">
が生成され、サイトマップに追加されます。
以上です。