From cb743f53e7f720d54e9dce944a3ad41617a79f83 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Wed, 7 Oct 2026 06:39:31 +0900 Subject: astro.config.mjs, package.json, scripts/indexnow.mjs: IndexNowとサイトマップlastmodを追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IndexNowのキーファイルと送信スクリプト、npm run indexnow、各ページの最終更新日をlastmodに反映。 --- astro.config.mjs | 45 +++++++++++++++++- package.json | 1 + public/b6f59342dacdbcb8d12be4b280873d19.txt | 1 + scripts/indexnow.mjs | 71 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 public/b6f59342dacdbcb8d12be4b280873d19.txt create mode 100644 scripts/indexnow.mjs diff --git a/astro.config.mjs b/astro.config.mjs index 46174d3..f6dd237 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,3 +1,4 @@ +import { execSync } from "node:child_process"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -45,6 +46,42 @@ function publicDirIndexPlugin() { }; } +// ページの最終更新日を git の履歴から引く(サイトマップの lastmod 用)。 +function buildGitDateMap() { + const map = new Map(); + try { + const out = execSync("git log --no-merges --pretty=format:__C__%cI --name-only", { + encoding: "utf-8", + maxBuffer: 256 * 1024 * 1024, + }); + let date = ""; + for (const raw of out.split("\n")) { + const line = raw.trim(); + if (line.startsWith("__C__")) { + date = line.slice(5); + } else if (line && date && !map.has(line)) { + map.set(line, date); + } + } + } catch { + // git が使えない環境では lastmod を付けない。 + } + return map; +} +const gitDates = buildGitDateMap(); + +function lastmodFor(url) { + const path = url.replace(/^https?:\/\/[^/]+/, "").replace(/^\/+|\/+$/g, ""); + const bases = path ? [path, `${path}/index`] : ["index"]; + for (const base of bases) { + for (const ext of [".mdx", ".md"]) { + const date = gitDates.get(`src/content/docs/${base}${ext}`); + if (date) return new Date(date); + } + } + return undefined; +} + export default defineConfig({ site: "https://yasutakeyohei.com", base: "/", @@ -61,7 +98,13 @@ export default defineConfig({ plugins: [publicDirIndexPlugin()], }, integrations: [ - sitemap(), + sitemap({ + serialize(item) { + const lastmod = lastmodFor(item.url); + if (lastmod) item.lastmod = lastmod; + return item; + }, + }), updatesIntegration(), ogImagePlugin(), starlight({ diff --git a/package.json b/package.json index ddbe175..046dbd5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "prebuild": "node scripts/optimize-images.mjs --apply", "build": "astro build && py -c \"import shutil; shutil.copy('public/.htaccess', 'dist/.htaccess')\" && py removeNullCharacters.py && node scripts/generate-llms-full.mjs", "deploy": "sh deploy.sh", + "indexnow": "node scripts/indexnow.mjs", "preview": "astro preview", "postinstall": "patch-package", "astro": "astro" diff --git a/public/b6f59342dacdbcb8d12be4b280873d19.txt b/public/b6f59342dacdbcb8d12be4b280873d19.txt new file mode 100644 index 0000000..8ee84ae --- /dev/null +++ b/public/b6f59342dacdbcb8d12be4b280873d19.txt @@ -0,0 +1 @@ +b6f59342dacdbcb8d12be4b280873d19 diff --git a/scripts/indexnow.mjs b/scripts/indexnow.mjs new file mode 100644 index 0000000..25bc768 --- /dev/null +++ b/scripts/indexnow.mjs @@ -0,0 +1,71 @@ +// IndexNow: URL を検索エンジン(Bing・Yandex・Seznam・Naver 等)へ即時通知する。 +// +// 使い方(デプロイしてキーファイルが公開された状態で実行する): +// 1) サイトマップの全URLを送る: npm run indexnow +// 2) 特定URLだけ送る: node scripts/indexnow.mjs https://yasutakeyohei.com/foo/ ... +// +// 仕様: https://www.indexnow.org/documentation +// キーは public/.txt(名前=中身)として公開する。 +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const HOST = "yasutakeyohei.com"; +const KEY_RE = /^[A-Za-z0-9-]{8,128}$/; + +// public/ から IndexNow キーファイル(ファイル名=中身の .txt)を探す。 +function findKey() { + if (process.env.INDEXNOW_KEY) return process.env.INDEXNOW_KEY; + for (const name of readdirSync(join(root, "public"))) { + if (!name.endsWith(".txt")) continue; + const stem = name.slice(0, -4); + if (!KEY_RE.test(stem)) continue; + if (readFileSync(join(root, "public", name), "utf-8").trim() === stem) { + return stem; + } + } + throw new Error("IndexNow キーファイル(public/.txt)が見つかりません"); +} + +// 送るURLを集める(引数があればそれ、無ければ dist のサイトマップ)。 +function collectUrls() { + const args = process.argv.slice(2).filter((a) => a.startsWith("http")); + if (args.length) return args; + const dir = join(root, "dist"); + const urls = []; + for (const name of readdirSync(dir)) { + if (!/^sitemap.*\.xml$/.test(name) || name === "sitemap-index.xml") continue; + const xml = readFileSync(join(dir, name), "utf-8"); + for (const m of xml.matchAll(/([^<]+)<\/loc>/g)) urls.push(m[1].trim()); + } + return urls; +} + +async function submit(endpoint, key, urls) { + const res = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify({ + host: HOST, + key, + keyLocation: `https://${HOST}/${key}.txt`, + urlList: urls, + }), + }); + return `${endpoint} -> ${res.status} ${res.statusText}`; +} + +const key = findKey(); +const urls = collectUrls(); +console.log(`IndexNow: key=${key} / urls=${urls.length}`); +for (const ep of [ + "https://api.indexnow.org/indexnow", + "https://www.bing.com/indexnow", +]) { + try { + console.log(" ", await submit(ep, key, urls)); + } catch (e) { + console.error(` ${ep} FAILED:`, e.message); + } +} -- cgit v1.3.1