Skip to content

Commit 1fd46c5

Browse files
committed
feat: IndexNow
1 parent 647e268 commit 1fd46c5

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

.env.sample

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ AUTH_GITHUB_SECRET=
1111
# 可选:用于访问 GitHub API(例如同步仓库)
1212
GITHUB_TOKEN=
1313

14+
# Bearer 鉴权Token
15+
INDEXNOW_API_TOKEN=
16+
#Open的Key
17+
INDEXNOW_KEY=5b6ef14a7406496b8a2ce8ab17820b34
18+
NEXT_PUBLIC_SITE_URL=https://involutionhell.vercel.app
1419
# Neon 提供的 Postgres 连接。
1520
# 登录 Neon 控制台 → 数据库 → "Connect" → "Connection details",可以复制以下所有变量。
1621
# 推荐连接字符串

.github/workflows/deploy.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
env:
1313
NEXT_TELEMETRY_DISABLED: "1"
1414
CI: "true"
15+
1516
steps:
1617
- uses: actions/checkout@v4
1718

@@ -28,3 +29,71 @@ jobs:
2829
- run: pnpm run lint
2930
- run: pnpm run lint:images
3031
- run: pnpm run typecheck
32+
33+
# === IndexNow 提交(仅在 main 分支执行) ===
34+
- name: Install jq (for JSON build)
35+
if: github.ref == 'refs/heads/main'
36+
run: sudo apt-get update && sudo apt-get install -y jq
37+
38+
- name: Submit IndexNow (changed URLs)
39+
if: github.ref == 'refs/heads/main'
40+
env:
41+
SITE_ORIGIN: https://involutionhell.vercel.app
42+
INDEXNOW_API: https://involutionhell.vercel.app/api/indexnow
43+
INDEXNOW_API_TOKEN: ${{ secrets.INDEXNOW_API_TOKEN }} # 若未启用鉴权可删
44+
run: |
45+
set -euo pipefail
46+
47+
# 获取本次代码变更
48+
git fetch --depth=2 origin ${{ github.ref }} || true
49+
CHANGED="$(git diff --name-only HEAD~1 HEAD || true)"
50+
51+
# 过滤属于文档或页面的变更(按需调整正则)
52+
CHANGED_DOCS="$(echo "$CHANGED" | grep -E '^(app/docs/|content/docs/).*\.(mdx?|tsx?)$' || true)"
53+
54+
URLS=()
55+
56+
# 规则 1:app/docs/**/page.(md/tsx) → /docs/**
57+
while IFS= read -r f; do
58+
[ -z "$f" ] && continue
59+
if [[ "$f" =~ ^app/docs/(.*)/page\.(mdx|md|tsx|ts|jsx|js)$ ]]; then
60+
slug="${BASH_REMATCH[1]}"
61+
URLS+=("$SITE_ORIGIN/docs/$slug")
62+
fi
63+
done <<< "$CHANGED_DOCS"
64+
65+
# 规则 2:content/docs/**.(md|mdx) → /docs/**(若不用 MDX 直出可删掉此段)
66+
while IFS= read -r f; do
67+
[ -z "$f" ] && continue
68+
if [[ "$f" =~ ^content/docs/(.*)\.(md|mdx)$ ]]; then
69+
slug="${BASH_REMATCH[1]}"
70+
slug="${slug%/index}"
71+
URLS+=("$SITE_ORIGIN/docs/$slug")
72+
fi
73+
done <<< "$CHANGED_DOCS"
74+
75+
# 去重
76+
mapfile -t URLS < <(printf "%s\n" "${URLS[@]}" | awk 'NF' | sort -u)
77+
78+
# 兜底:没有匹配就推首页
79+
if [ "${#URLS[@]}" -eq 0 ]; then
80+
URLS=("$SITE_ORIGIN/")
81+
fi
82+
83+
echo "✅ Submitting URLs to IndexNow:"
84+
printf ' - %s\n' "${URLS[@]}"
85+
86+
# 组 JSON
87+
JSON="$(jq -n --argjson arr "$(printf '%s\n' "${URLS[@]}" | jq -R . | jq -s .)" '{urlList: $arr}')"
88+
89+
# 可选鉴权
90+
AUTH_HEADER=()
91+
if [ -n "${INDEXNOW_API_TOKEN:-}" ]; then
92+
AUTH_HEADER=(-H "Authorization: Bearer ${INDEXNOW_API_TOKEN}")
93+
fi
94+
95+
# 调用你的 /api/indexnow
96+
curl -sS -X POST "$INDEXNOW_API" \
97+
-H "Content-Type: application/json" \
98+
"${AUTH_HEADER[@]}" \
99+
-d "$JSON"

app/api/indexnow/route.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// app/api/indexnow/route.ts
2+
// Next.js App Router API route for IndexNow push (Serverless friendly)
3+
4+
export const runtime = "edge"; // 也可用 "nodejs",两者都能 fetch
5+
6+
type Payload = {
7+
url?: string;
8+
urls?: string[];
9+
urlList?: string[];
10+
host?: string;
11+
engine?: "bing" | "yandex" | "seznam" | "naver" | "hub";
12+
};
13+
14+
// 可选:简单的调用鉴权,避免被他人滥用。到 Vercel 环境变量里设置 INDEXNOW_API_TOKEN。
15+
const REQUIRED_BEARER = process.env.INDEXNOW_API_TOKEN ?? "";
16+
17+
// 你的 IndexNow 验证 key(即 public/{key}.txt 的文件名部分)
18+
const INDEXNOW_KEY =
19+
process.env.INDEXNOW_KEY ?? process.env.NEXT_PUBLIC_INDEXNOW_KEY ?? "";
20+
21+
function bad(msg: string, status = 400) {
22+
return new Response(JSON.stringify({ ok: false, error: msg }), {
23+
status,
24+
headers: { "content-type": "application/json; charset=utf-8" },
25+
});
26+
}
27+
28+
function ok(data: unknown) {
29+
return new Response(
30+
JSON.stringify({ ok: true, ...((data as object) ?? {}) }),
31+
{
32+
status: 200,
33+
headers: { "content-type": "application/json; charset=utf-8" },
34+
},
35+
);
36+
}
37+
38+
function dedupeValidHttps(urls: (string | undefined)[]) {
39+
const s = new Set<string>();
40+
for (const u of urls) {
41+
if (!u) continue;
42+
try {
43+
const url = new URL(u);
44+
if (url.protocol === "https:" || url.protocol === "http:") {
45+
s.add(url.toString());
46+
}
47+
} catch {
48+
console.error("Invalid URL:", u);
49+
}
50+
}
51+
return [...s];
52+
}
53+
54+
function getDefaultHostFromReq(req: Request) {
55+
// 从请求头推断 Host & Protocol(在 Vercel 上可用)
56+
const host =
57+
req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "";
58+
const proto = req.headers.get("x-forwarded-proto") ?? "https";
59+
return { host, proto };
60+
}
61+
62+
function endpointFor(engine?: Payload["engine"]) {
63+
switch (engine) {
64+
case "bing":
65+
return "https://www.bing.com/indexnow";
66+
case "yandex":
67+
return "https://yandex.com/indexnow";
68+
case "seznam":
69+
return "https://search.seznam.cz/indexnow";
70+
case "naver":
71+
return "https://searchadvisor.naver.com/indexnow";
72+
case "hub":
73+
default:
74+
return "https://api.indexnow.org/indexnow"; // 官方 Hub(推荐)
75+
}
76+
}
77+
78+
async function pushToIndexNow({
79+
host,
80+
key,
81+
urlList,
82+
engine,
83+
}: {
84+
host: string;
85+
key: string;
86+
urlList: string[];
87+
engine?: Payload["engine"];
88+
}) {
89+
const keyLocation = `https://${host}/${key}.txt`;
90+
const endpoint = endpointFor(engine);
91+
92+
const res = await fetch(endpoint, {
93+
method: "POST",
94+
headers: { "content-type": "application/json" },
95+
body: JSON.stringify({
96+
host,
97+
key,
98+
keyLocation,
99+
urlList,
100+
}),
101+
});
102+
103+
const text = await res.text().catch(() => "");
104+
return { status: res.status, ok: res.ok, body: text };
105+
}
106+
107+
// 便捷:GET ?url=https://your-site/page 也可以触发一次推送
108+
export async function GET(req: Request) {
109+
if (REQUIRED_BEARER) {
110+
const auth = req.headers.get("authorization") || "";
111+
if (!auth.startsWith("Bearer ") || auth.slice(7) !== REQUIRED_BEARER) {
112+
return bad("Unauthorized", 401);
113+
}
114+
}
115+
if (!INDEXNOW_KEY) return bad("Missing INDEXNOW_KEY env", 500);
116+
117+
const { host: defaultHost } = getDefaultHostFromReq(req);
118+
if (!defaultHost) return bad("Cannot infer host from request", 500);
119+
120+
const urlParam = new URL(req.url).searchParams.get("url") ?? undefined;
121+
const urls = dedupeValidHttps([urlParam]);
122+
if (urls.length === 0) return bad("Provide ?url=...", 400);
123+
124+
const result = await pushToIndexNow({
125+
host: defaultHost,
126+
key: INDEXNOW_KEY,
127+
urlList: urls,
128+
engine: "hub",
129+
});
130+
131+
return ok({ indexnow: result, submitted: urls });
132+
}
133+
134+
// 推荐:POST 更灵活,支持批量
135+
export async function POST(req: Request) {
136+
if (REQUIRED_BEARER) {
137+
const auth = req.headers.get("authorization") || "";
138+
if (!auth.startsWith("Bearer ") || auth.slice(7) !== REQUIRED_BEARER) {
139+
return bad("Unauthorized", 401);
140+
}
141+
}
142+
if (!INDEXNOW_KEY) return bad("Missing INDEXNOW_KEY env", 500);
143+
144+
let body: Payload = {};
145+
try {
146+
body = (await req.json()) as Payload;
147+
} catch {
148+
console.error("Error parsing request body");
149+
}
150+
151+
const { host: inferredHost } = getDefaultHostFromReq(req);
152+
const host = (body.host || inferredHost || "").replace(/^https?:\/\//, "");
153+
if (!host) return bad("Cannot infer host; pass { host }", 400);
154+
155+
const urls = dedupeValidHttps([
156+
body.url,
157+
...(body.urls || []),
158+
...(body.urlList || []),
159+
]);
160+
if (urls.length === 0) return bad("Provide { url } or { urls/urlList }", 400);
161+
162+
const result = await pushToIndexNow({
163+
host,
164+
key: INDEXNOW_KEY,
165+
urlList: urls,
166+
engine: body.engine ?? "hub",
167+
});
168+
169+
return ok({ indexnow: result, submitted: urls, host });
170+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5b6ef14a7406496b8a2ce8ab17820b34

0 commit comments

Comments
 (0)