検索エンジンにWebサイト内のファイル情報を伝えるXMLサイトマップ。
Google検索セントラルによると、必須というわけではなさそうですが、SEO対策として作成しています。
今まではサイトのローンチ時や、新規ページ作成時にオンラインジェネレーターで作成していました。
そんな中、<lastmod>
はページの更新日時を指定する必要があることを知りました。
ページを更新するたびにオンラインジェネレーターで作成するのは面倒すぎる…
というわけで、<lastmod>
の時間にGitのコミット日時を反映したXMLサイトマップを自動生成できるようにしてみました。
仕様
XMLサイトマップに含めるファイルは任意のディレクトリ以下のファイルを対象とします。
<lastmod>
にはGitの最終コミット日時が反映されます。
- XMLサイトマップに含めるファイル
-
src
ディレクトリ内の.html
と.pdf
(デフォルト)- ディレクトリ名は変更可能(オプション)
- 対象ファイルは変更可能(オプション)
- 対象ファイルはgitのコミットログが取得可能(必須)
-
- XMLサイトマップのファイル名
-
sitemap.xml
(デフォルト)- ファイル名は変更可能(オプション)
-
- XMLサイトマップの出力ディレクトリ
-
dist
ディレクトリに生成(デフォルト)- ディレクトリ名は変更可能(オプション)
- ディレクトリが存在しない場合は自動で生成
-
XMLサイトマップの構造
作成されるXMLサイトマップの構造は以下となります。
Googleは、<priority>
と <changefreq>
の値を無視するため、各 <url>
には <loc>
と <lastmod>
だけ指定しています。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2024-02-03T00:00:00+09:00</lastmod>
</url>
</urlset>
ファイル構造
デフォルト設定では以下のような構造になります。
project
├── .git
├── dist/
│ └── sitemap.xml
├── node_modules
├── src/
│ ├── index.html
│ ├── hoge
│ │ └── index.html
│ ├── fuga
│ │ └── index.html
│ └── piyo
│ └── index.html
├── _xml-sitemap-generator.bat
├── xml-sitemap-generator.js
├── package.json
└── package-lock.json
準備
package.json の作成
{
"name": "xml-sitemap-generator",
"version": "1.0.0",
"license": "UNLICENSED",
"private": true,
"scripts": {
"xml-sitemap-generator": "node ./xml-sitemap-generator.js"
},
"type": "module"
}
node_module のインストール
必要な node_module
は以下となります。
モジュール名 | 役割 |
---|---|
ansi-colors | ログメッセージに色をつける |
fancy-log | 時刻付きのログを表示 |
globule | ファイルのパスを取得 |
child_process | ファイルのコミット日時を取得 |
date-fns | 日時をISO 8601形式に変換 |
util | 非同期処理 |
npm i ansi-colors fancy-log globule child_process date-fns util -D
npm-scripts の作成
import c from 'ansi-colors'
import log from 'fancy-log'
import fs from 'fs/promises'
import globule from 'globule'
import { exec } from 'child_process'
import { format } from 'date-fns'
import { promisify } from 'util'
const execAsync = promisify(exec)
class XmlSitemapGenerator {
#defaults = {
siteUrl: null,
srcDir: 'src',
distDir: 'dist',
src: ['/**/*.{html,shtml,pdf}'],
fileName: 'sitemap.xml',
mappings: []
}
#options
constructor(options = {}) {
this.#options = { ...this.#defaults, ...options }
try {
if (!this.#options.siteUrl) {
throw new Error()
}
this.#init()
} catch (error) {
log(
c.red('ERROR! ') +
`'` +
c.yellow('siteUrl') +
`'` +
' option is required but not provided.'
)
}
}
async #init() {
try {
const fileList = await this.#getFileList()
const sitemapContent = this.#generateSitemapContent(fileList)
await this.#writeSitemapFile(sitemapContent)
} catch (error) {
log(c.red('ERROR! ') + error.message)
}
}
/**
* 対象ファイルのパスとコミット日時を持つオブジェクトの配列を返す
* @return { array } 対象ファイルの配列
*/
async #getFileList() {
this.#options.src = this.#options.src.map(
(src) => `${this.#options.srcDir}${src}`
)
const files = globule.find({ src: this.#options.src })
const filePromises = files.map(async (file) => {
try {
const { stdout } = await execAsync(`git log -1 --format=%cI "${file}"`)
const gitFromTime = stdout.trim()
const formattedTime = format(
new Date(gitFromTime),
"yyyy-MM-dd'T'HH:mm:ssXXX"
)
return {
file,
lastmod: formattedTime
}
} catch (error) {
log(`No Git commit found for file: '` + c.yellow(file) + `'`)
return null
}
})
const fileList = await Promise.all(filePromises)
return fileList.filter(Boolean)
}
/**
* XMLサイトマップのコンテンツを返す
* @param { array } fileList 対象ファイルの配列
* @return { string } XMLサイトマップのコンテンツ
*/
#generateSitemapContent(fileList) {
const sitemapHeader = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`
const sitemapFooter = `</urlset>`
const sitemapBody = this.#getSitemapBody(fileList)
return sitemapHeader + sitemapBody + sitemapFooter
}
/**
* すべての<url>要素の文字列を返す
* @param { array } fileList 対象ファイルの配列
* @return { string } すべての<url>要素の文字列
*/
#getSitemapBody(fileList) {
return fileList
.map(({ file, lastmod }) => this.#getLocAndLastmod(file, lastmod))
.sort((a, b) => a.loc.localeCompare(b.loc))
.map(({ loc, lastmod }) => this.#formatUrlElement(loc, lastmod))
.join('')
}
/**
* <loc>と<lastmod>の値を持つオブジェクトを返す
* @param { string } file 対象ファイルのパス
* @param { string } lastmod 対象ファイルのコミット日時
* @return { object } <loc>と<lastmod>の値を持つオブジェクト
*/
#getLocAndLastmod(file, lastmod) {
let loc = file.replace(this.#options.srcDir, this.#options.siteUrl)
this.#options.mappings.forEach((mapping) => {
if (loc.includes(mapping.fromPath)) {
loc = loc.replace(mapping.fromPath, mapping.toPath)
}
})
// <loc>の末尾にindex.htmlとindex.phpが含まれていたら削除
if (loc.endsWith('index.html') || loc.endsWith('index.php')) {
loc = loc.replace(/index\.(html|php)$/, '')
}
return { loc, lastmod }
}
/**
* <url>要素の文字列を返す
* @param { string } loc 対象ファイルのパス
* @param { string } lastmod 対象ファイルのコミット日時
* @return { string } <url>要素の文字列
*/
#formatUrlElement(loc, lastmod) {
return ` <url>\n <loc>${loc}</loc>\n <lastmod>${lastmod}</lastmod>\n </url>\n`
}
/**
* XMLサイトマップの作成
*/
async #writeSitemapFile(content) {
try {
// ディレクトリが存在しない場合は作成する
await fs.mkdir(this.#options.distDir, { recursive: true })
await fs.writeFile(
`${this.#options.distDir}/${this.#options.fileName}`,
`${content}\n`
)
const urlCount = (content.match(/<url>/g) || []).length
log(
`Created '` +
c.green(`${this.#options.distDir}/${this.#options.fileName}`) +
`' in ` +
c.green(urlCount) +
` <url> elements.`
)
} catch (error) {
log.error(
c.red('ERROR! ') +
`Failed to create '` +
c.yellow(`${this.#options.distDir}/${this.#options.fileName}`) +
` '${error}`
)
}
}
}
const xmlSitemapGenerator = new XmlSitemapGenerator({
siteUrl: 'https://example.com',
})
オプション
インスタンス作成時、以下のオプション設定が可能です。
siteUrl
は必須オプションとなります。
オプション | 内容 | 型 | デフォルト |
---|---|---|---|
siteUrl | サイトのURL | string | null |
srcDir | XMLサイトマップに含めるファイルの格納ディレクトリ名 | string | 'src' |
distDir | XMLサイトマップの出力ディレクトリ名 | string | 'dist' |
src | XMLサイトマップに含めるファイルのglobパターンsrcDir 以下を指定 |
array |
srcDir 内のすべてのHTMLとPDF |
fileName | XMLサイトマップのファイル名 | string | 'sitemap.xml' |
mappings | 指定したファイルを任意のURLにマッピングさせます | array | [] |
const xmlSitemapGenerator = new XmlSitemapGenerator({
// サイトのURL
siteUrl: 'https://example.com',
// XMLサイトマップに含めるファイルの格納ディレクトリ変更
srcDir: 'hoge',
// XMLサイトマップの出力ディレクトリ変更
distDir: 'hoge',
// XMLサイトマップに含めるファイルを変更(srcDir以下のパスを指定)
src: [
'/!(_wp)/**/*.html',
'/assets/**/*.pdf',
'/_wp/wp-content/themes/original-theme/front-page.php',
'/_wp/wp-content/themes/original-theme/index.php'
],
// 指定したファイルを任意のURLにマッピング
mappings: [
{
fromPath: '/_wp/wp-content/themes/original-theme/front-page.php',
toPath: '/'
},
{
fromPath: '/_wp/wp-content/themes/original-theme/index.php',
toPath: '/news/'
}
]
})
バッチファイル の作成(Windows)
Windows のみとなります。
npm-scripts の実行を簡単にするためにバッチファイルを作成します。
@echo off
PowerShell -command npm run xml-sitemap-generator
pause
npm-scripts の実行
Windows
バッチファイル( _xml-sitemap-generator.bat
)をダブルクリックでOK
Mac
ターミナルで xml-sitemap-generator.js
ファイルのあるディレクトリ移動し、以下コマンドを実行します。
npm run xml-sitemap-generator