aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/migrate-to-question-summary.mjs
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-06-21 19:35:30 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-06-21 19:35:30 +0900
commit6327783554018e69a6d8abd36f6ea538df6ee9f3 (patch)
treedafeed0ae1dee8bb5de5aab2abe25f7218e1a7a3 /scripts/migrate-to-question-summary.mjs
parent32e2ab7749480d294b79e1e550daae07b778d1d1 (diff)
一般質問ページに FAQPage 構造化データを追加
QuestionSummary コンポーネントを導入し、63ページの一般質問に Schema.org FAQPage JSON-LD を追加。表と構造化データを単一の データ源から生成するため、表の更新と JSON-LD の同期が自動化される。 - src/components/QuestionSummary.astro: 新規。表と JSON-LD を生成 - src/components/StructuredQA.astro: 削除(QuestionSummary に置換) - scripts/migrate-to-question-summary.mjs: 旧 Markdown 表の変換用 - src/content/docs/ippan-situmon/**/*.mdx: 63ファイル移行済み 新規ページ作成時は QuestionSummary コンポーネントを使用する。 Markdown 表を直接書くことは禁止(コンポーネントにルールを明記)。
Diffstat (limited to 'scripts/migrate-to-question-summary.mjs')
-rw-r--r--scripts/migrate-to-question-summary.mjs187
1 files changed, 187 insertions, 0 deletions
diff --git a/scripts/migrate-to-question-summary.mjs b/scripts/migrate-to-question-summary.mjs
new file mode 100644
index 0000000..75cabd2
--- /dev/null
+++ b/scripts/migrate-to-question-summary.mjs
@@ -0,0 +1,187 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { readdir } from "node:fs/promises";
+import { join, extname, relative } from "node:path";
+
+const DOCS_DIR = join(import.meta.dirname, "..", "src", "content", "docs");
+
+/**
+ * Recursively collect all .mdx files, excluding files starting with "_" or "index".
+ */
+async function collectMdxFiles(dir) {
+ const entries = await readdir(dir, { withFileTypes: true });
+ const files = [];
+
+ for (const entry of entries) {
+ if (entry.name.startsWith("_")) continue;
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ const subFiles = await collectMdxFiles(fullPath);
+ files.push(...subFiles);
+ } else if (extname(entry.name) === ".mdx" && entry.name !== "index.mdx") {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+/**
+ * Extract Q&A rows from the summary table in the MDX content.
+ * Table format:
+ * | 質問 | 答弁概要(クリックで詳細) |
+ * |---|---|
+ * | ① question text | [answer text](#anchor) |
+ */
+function extractQATable(content) {
+ // Find the summary table: starts with "| 質問 | 答弁概要"
+ const tableStartRegex =
+ /\| 質問 \| 答弁概要(クリックで詳細) \|\r?\n\|[-| ]+\|\r?\n/;
+ const tableStartMatch = content.match(tableStartRegex);
+ if (!tableStartMatch) return null;
+
+ const tableStart = tableStartMatch.index;
+ const afterHeader = content.slice(tableStart + tableStartMatch[0].length);
+
+ // Collect table rows
+ const rows = [];
+ const lines = afterHeader.split(/\r?\n/);
+ for (const line of lines) {
+ const rowMatch = line.match(/^\| (.+?) \| \[(.+?)\]\((#.+?)\)\s*\|$/);
+ if (!rowMatch) break;
+ rows.push({
+ question: rowMatch[1].trim(),
+ answer: rowMatch[2].trim(),
+ anchor: rowMatch[3].replace(/^#/, "").trim(),
+ });
+ }
+
+ if (rows.length === 0) return null;
+
+ // Calculate the end of the table (header + separator + all rows)
+ const tableText = content.slice(tableStart);
+ const tableEndMatch = tableText.match(
+ new RegExp(
+ `(?:^\\| .+? \\| \\[.+?\\]\\(#.+?\\)\\s*\\|$(?:\\r?\\n)?){${rows.length}}`,
+ "m",
+ ),
+ );
+ if (!tableEndMatch) return null;
+
+ const tableEnd =
+ tableStart + tableStartMatch[0].length + tableEndMatch[0].length;
+
+ return {
+ rows,
+ tableStart,
+ tableEnd,
+ };
+}
+
+/**
+ * Extract frontmatter fields from MDX content.
+ */
+function extractFrontmatter(content) {
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
+ if (!match) return {};
+ const fm = match[1];
+
+ const titleMatch = fm.match(/^title:\s*["']?(.+?)["']?\s*$/m);
+ const dateMatch = fm.match(
+ /^(?:first|date):\s*["']?(\d{4}-\d{2}-\d{2})["']?\s*$/m,
+ );
+
+ return {
+ title: titleMatch ? titleMatch[1].trim() : null,
+ date: dateMatch ? dateMatch[1].trim() : null,
+ };
+}
+
+/**
+ * Ensure the import line for QuestionSummary exists in the import block.
+ */
+function addImport(content, importPath) {
+ const importLine = `import QuestionSummary from '${importPath}';`;
+
+ if (content.includes("QuestionSummary")) {
+ return content; // Already imported
+ }
+
+ // Find the last import statement and insert after it
+ const importRegex = /^(import .+)$/gm;
+ const imports = [...content.matchAll(importRegex)];
+ if (imports.length > 0) {
+ const lastImport = imports[imports.length - 1];
+ const insertPos = lastImport.index + lastImport[0].length;
+ return (
+ content.slice(0, insertPos) + "\n" + importLine + content.slice(insertPos)
+ );
+ }
+
+ return content; // No existing imports
+}
+
+/**
+ * Generate the QuestionSummary component JSX string.
+ */
+function generateQuestionSummary(qaRows, headline, datePublished) {
+ const qaEntries = qaRows
+ .map((row) => {
+ return ` { question: ${JSON.stringify(row.question)}, answer: ${JSON.stringify(row.answer)}, anchor: ${JSON.stringify(row.anchor)} },`;
+ })
+ .join("\n");
+
+ const props = [];
+ if (headline) props.push(` headline=${JSON.stringify(headline)}`);
+ if (datePublished)
+ props.push(` datePublished=${JSON.stringify(datePublished)}`);
+
+ return `<QuestionSummary\n${props.join("\n")}\n qa={[\n${qaEntries}\n ]}\n/>`;
+}
+
+async function main() {
+ const files = await collectMdxFiles(DOCS_DIR);
+ console.log(`Found ${files.length} MDX files to check.`);
+
+ let migrated = 0;
+ let skipped = 0;
+
+ for (const file of files) {
+ const content = readFileSync(file, "utf-8");
+ const tableData = extractQATable(content);
+
+ if (!tableData) {
+ skipped++;
+ continue;
+ }
+
+ const fm = extractFrontmatter(content);
+
+ // Generate the replacement component
+ const component = generateQuestionSummary(
+ tableData.rows,
+ fm.title,
+ fm.date,
+ );
+
+ // Replace the table with the component
+ let newContent =
+ content.slice(0, tableData.tableStart) +
+ component +
+ content.slice(tableData.tableEnd);
+
+ // Ensure the import exists
+ newContent = addImport(newContent, "@/components/QuestionSummary.astro");
+
+ writeFileSync(file, newContent, "utf-8");
+ const relPath = relative(DOCS_DIR, file);
+ console.log(` Migrated: ${relPath} (${tableData.rows.length} QA pairs)`);
+ migrated++;
+ }
+
+ console.log(`\nDone: ${migrated} migrated, ${skipped} skipped.`);
+}
+
+main().catch((err) => {
+ console.error("Error:", err);
+ process.exit(1);
+});