diff --git a/app/components/GiscusComments.tsx b/app/components/GiscusComments.tsx
index ef8d60a..9400f99 100644
--- a/app/components/GiscusComments.tsx
+++ b/app/components/GiscusComments.tsx
@@ -5,21 +5,11 @@ import Giscus from "@giscus/react";
interface GiscusCommentsProps {
className?: string;
docId?: string | null;
- title?: string | null;
}
-export function GiscusComments({
- className,
- docId,
- title,
-}: GiscusCommentsProps) {
+export function GiscusComments({ className, docId }: GiscusCommentsProps) {
const normalizedDocId = typeof docId === "string" ? docId.trim() : "";
- const normalizedTitle = typeof title === "string" ? title.trim() : "";
-
const useSpecificMapping = normalizedDocId.length > 0;
- const termValue = useSpecificMapping
- ? `${normalizedTitle || "Untitled"} | ${normalizedDocId}`
- : undefined;
return (
@@ -29,7 +19,7 @@ export function GiscusComments({
category="Comments"
categoryId="DIC_kwDOPuD_8M4Cvip8"
mapping={useSpecificMapping ? "specific" : "pathname"}
- term={termValue}
+ term={useSpecificMapping ? normalizedDocId : undefined}
strict="0"
reactionsEnabled="1"
emitMetadata="0"
diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx
index 98256d4..0d20f06 100644
--- a/app/docs/[...slug]/page.tsx
+++ b/app/docs/[...slug]/page.tsx
@@ -57,7 +57,6 @@ export default async function DocPage({ params }: Param) {
const contributorsEntry =
getDocContributorsByPath(page.file.path) ||
getDocContributorsByDocId(docIdFromPage);
- const discussionTitle = page.data.title ?? docIdFromPage ?? page.path;
const Mdx = page.data.body;
// Prepare page content for AI assistant
@@ -87,10 +86,7 @@ export default async function DocPage({ params }: Param) {
diff --git a/scripts/test.mjs b/scripts/test.mjs
index b695817..78e9de9 100644
--- a/scripts/test.mjs
+++ b/scripts/test.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
- * 将 GitHub Discussions 标题补上 [docId: ],用于从 pathname->docId 的 Giscus 迁移。
+ * 将 GitHub Discussions 标题统一重写为 docId,用于从 pathname->docId 的 Giscus 迁移。
*
* 两种输入来源:
* A) DB 模式(推荐):读取 Postgres(docs/path_current + doc_paths)获得每个 docId 的所有历史路径
@@ -21,6 +21,12 @@
* # 用映射文件(不连 DB)
* node scripts/migrate-giscus-add-docid.mjs --map=tmp/discussion-map.json --apply=true
*
+ * # 仅处理部分 doc,支持多次传参或逗号/换行分隔
+ * node scripts/migrate-giscus-add-docid.mjs --doc=abcd123 --doc=efg456 --apply=true
+ * node scripts/migrate-giscus-add-docid.mjs --doc-path=app/docs/foo/bar.mdx --doc-path=docs/foo/bar --apply=true
+ * node scripts/migrate-giscus-add-docid.mjs --doc-paths="app/docs/foo/bar.mdx,docs/foo/bar" --apply=true
+ * GISCUS_DOC_PATHS="app/docs/foo/bar.mdx\napp/docs/baz.mdx" node scripts/migrate-giscus-add-docid.mjs --apply=true
+ *
* 映射文件格式(示例):
* {
* "i0xmpsk...xls": ["app/docs/foo/bar.mdx", "/docs/foo/bar"],
@@ -52,6 +58,17 @@ const REPO =
const MAP = getArg("map") || process.env.GISCUS_DISCUSSION_MAP || ""; // JSON 文件(映射文件模式)
const APPLY = (getArg("apply") || "false").toLowerCase() === "true"; // 是否真的更新标题
+const DOC_FILTERS = getArgList("doc");
+const DOC_PATH_FILTERS = [
+ ...getArgList("doc-path"),
+ ...getArgList("doc-paths"),
+ ...(process.env.GISCUS_DOC_PATHS
+ ? process.env.GISCUS_DOC_PATHS.split(/[,\n]/)
+ .map((v) => v.trim())
+ .filter(Boolean)
+ : []),
+];
+
if (!GH_TOKEN) {
console.error("[migrate-giscus] Missing GH_TOKEN/GITHUB_TOKEN.");
process.exit(1);
@@ -62,6 +79,21 @@ function getArg(k) {
return arg ? arg.split("=")[1] : null;
}
+function getArgList(k) {
+ const matches = process.argv
+ .slice(2)
+ .filter((s) => s.startsWith(`--${k}=`))
+ .map((s) => s.split("=")[1]);
+ if (matches.length === 0) {
+ const single = getArg(k);
+ if (single) matches.push(single);
+ }
+ return matches
+ .flatMap((value) => (value ?? "").split(/[,\n]/))
+ .map((value) => value.trim())
+ .filter(Boolean);
+}
+
const GQL = "https://api.github.com/graphql";
const ghHeaders = {
"Content-Type": "application/json",
@@ -126,21 +158,21 @@ async function loadDocIdTerms() {
select: {
id: true,
path_current: true,
+ title: true,
doc_paths: { select: { path: true } },
},
});
- const map = new Map(); // docId -> Set
+ const map = new Map(); // docId -> { title: string|null, terms: Set }
for (const d of docs) {
- const set = map.get(d.id) ?? new Set();
- if (d.path_current) set.add(d.path_current);
- for (const p of d.doc_paths) if (p?.path) set.add(p.path);
- // 兼容站点实际的 pathname(可选添加去掉扩展名、加前缀)
- for (const p of Array.from(set)) {
- const noExt = p.replace(/\.(md|mdx|markdown)$/i, "");
- set.add(noExt);
- set.add(`/${noExt}`); // 常见 pathname 形态
- }
- map.set(d.id, set);
+ const entry = map.get(d.id) ?? {
+ title: d.title ?? null,
+ terms: new Set(),
+ };
+ if (!entry.title && d.title) entry.title = d.title;
+ if (d.path_current) registerPathVariants(entry.terms, d.path_current);
+ for (const p of d.doc_paths)
+ if (p?.path) registerPathVariants(entry.terms, p.path);
+ map.set(d.id, entry);
}
return map;
}
@@ -151,17 +183,26 @@ async function loadDocIdTerms() {
const raw = await fs.readFile(abs, "utf8");
const obj = JSON.parse(raw);
const map = new Map();
- for (const [docId, arr] of Object.entries(obj)) {
- const set = new Set();
- (arr || []).forEach((t) => {
- if (typeof t === "string" && t.trim()) {
- set.add(t.trim());
- const noExt = t.replace(/\.(md|mdx|markdown)$/i, "");
- set.add(noExt);
- set.add(`/${noExt}`);
+ for (const [docId, rawValue] of Object.entries(obj)) {
+ const entry = { title: null, terms: new Set() };
+
+ if (Array.isArray(rawValue)) {
+ rawValue.forEach((t) => registerPathVariants(entry.terms, t));
+ } else if (rawValue && typeof rawValue === "object") {
+ if (typeof rawValue.title === "string" && rawValue.title.trim()) {
+ entry.title = rawValue.title.trim();
+ }
+ const termsSource = Array.isArray(rawValue.terms)
+ ? rawValue.terms
+ : rawValue.paths;
+ if (Array.isArray(termsSource)) {
+ termsSource.forEach((t) => registerPathVariants(entry.terms, t));
}
- });
- map.set(docId, set);
+ } else if (typeof rawValue === "string") {
+ registerPathVariants(entry.terms, rawValue);
+ }
+
+ map.set(docId, entry);
}
return map;
}
@@ -183,18 +224,90 @@ async function searchDiscussionByTerm(term) {
);
}
-// 如果标题中已经包含 [docId: xxx],就跳过
-function alreadyHasDocIdTag(title, docId) {
- const tag = `[docId:${docId}]`;
- return title.includes(tag);
+function titleAlreadyNormalized(title, docId) {
+ const normalized = docId.trim();
+ if (!normalized) return false;
+ return title.trim() === normalized;
}
-// 生成新标题(在末尾追加,如已含则不变)
-function appendDocIdTag(title, docId) {
- const tag = `[docId:${docId}]`;
- if (title.includes(tag)) return title;
- // 避免标题太挤,加个空格
- return `${title.trim()} ${tag}`;
+function normalizeTitleToDocId(currentTitle, docId) {
+ const normalized = docId.trim();
+ if (!normalized) return currentTitle.trim();
+ return normalized;
+}
+
+function registerPathVariants(targetSet, rawPath) {
+ if (typeof rawPath !== "string") return;
+ const trimmed = rawPath.trim();
+ if (!trimmed) return;
+
+ const variants = new Set();
+ const candidates = [trimmed];
+
+ const withoutExt = trimmed.replace(/\.(md|mdx|markdown)$/i, "");
+ candidates.push(withoutExt);
+
+ const leadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
+ candidates.push(leadingSlash);
+
+ const withoutExtLeadingSlash = withoutExt.startsWith("/")
+ ? withoutExt
+ : `/${withoutExt}`;
+ candidates.push(withoutExtLeadingSlash);
+
+ const withoutApp = trimmed.replace(/^app\//i, "");
+ if (withoutApp && withoutApp !== trimmed) {
+ candidates.push(withoutApp);
+ const withoutAppNoExt = withoutApp.replace(/\.(md|mdx|markdown)$/i, "");
+ candidates.push(withoutAppNoExt);
+ candidates.push(withoutApp.startsWith("/") ? withoutApp : `/${withoutApp}`);
+ candidates.push(
+ withoutAppNoExt.startsWith("/") ? withoutAppNoExt : `/${withoutAppNoExt}`,
+ );
+ }
+
+ for (const candidate of candidates) {
+ const value = typeof candidate === "string" ? candidate.trim() : "";
+ if (value) variants.add(value);
+ }
+
+ for (const value of variants) targetSet.add(value);
+}
+
+function applyFilters(docIdMap) {
+ const docIdFilterSet = new Set(DOC_FILTERS);
+ const hasDocIdFilter = docIdFilterSet.size > 0;
+
+ const pathFilterVariants = new Set();
+ for (const path of DOC_PATH_FILTERS) {
+ registerPathVariants(pathFilterVariants, path);
+ }
+ const hasPathFilter = pathFilterVariants.size > 0;
+
+ if (!hasDocIdFilter && !hasPathFilter) {
+ return;
+ }
+
+ for (const [docId, info] of Array.from(docIdMap.entries())) {
+ let keep = true;
+
+ if (keep && hasDocIdFilter) {
+ keep = docIdFilterSet.has(docId);
+ }
+
+ if (keep && hasPathFilter) {
+ const terms = Array.from(info?.terms ?? []);
+ keep = terms.some((term) => pathFilterVariants.has(term));
+ }
+
+ if (!keep) {
+ docIdMap.delete(docId);
+ }
+ }
+
+ if (docIdMap.size === 0) {
+ log("⚠️ 未找到符合过滤条件的 docId,本次执行不会更新任何讨论。");
+ }
}
async function main() {
@@ -203,13 +316,20 @@ async function main() {
);
const docIdToTerms = await loadDocIdTerms();
+ applyFilters(docIdToTerms);
+
+ if (docIdToTerms.size === 0) {
+ if (prisma) await prisma.$disconnect();
+ return;
+ }
+
let updated = 0,
skipped = 0,
notFound = 0,
examined = 0;
- for (const [docId, termsSet] of docIdToTerms) {
- const terms = Array.from(termsSet);
+ for (const [docId, info] of docIdToTerms) {
+ const terms = Array.from(info?.terms ?? []);
let matched = null;
// 尝试每个 term,直到命中一个讨论
@@ -235,13 +355,13 @@ async function main() {
examined += 1;
const oldTitle = matched.title;
- if (alreadyHasDocIdTag(oldTitle, docId)) {
+ if (titleAlreadyNormalized(oldTitle, docId)) {
skipped += 1;
log(`⏭ #${matched.number} 已包含 docId:${matched.url}`);
continue;
}
- const newTitle = appendDocIdTag(oldTitle, docId);
+ const newTitle = normalizeTitleToDocId(oldTitle, docId);
log(
`${APPLY ? "✏️ 更新" : "👀 预览"} #${matched.number} "${oldTitle}" → "${newTitle}"`,
);