diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-06-21 19:35:30 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-06-21 19:35:30 +0900 |
| commit | 6327783554018e69a6d8abd36f6ea538df6ee9f3 (patch) | |
| tree | dafeed0ae1dee8bb5de5aab2abe25f7218e1a7a3 /scripts | |
| parent | 32e2ab7749480d294b79e1e550daae07b778d1d1 (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')
| -rw-r--r-- | scripts/migrate-to-question-summary.mjs | 187 |
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); +}); |
