aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--astro.config.mjs45
-rw-r--r--package.json1
-rw-r--r--public/b6f59342dacdbcb8d12be4b280873d19.txt1
-rw-r--r--scripts/indexnow.mjs71
4 files changed, 117 insertions, 1 deletions
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/<key>.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/<key>.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>([^<]+)<\/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);
+ }
+}