import { execSync } from "node:child_process"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; import sitemap from "@astrojs/sitemap"; import { unified } from "@astrojs/markdown-remark"; import remarkMermaid from "remark-mermaidjs"; import ogImagePlugin from "./src/plugins/og-image"; import updatesIntegration from "./src/plugins/updates"; // Apache (production) resolves a directory URL to its index.html, so the site can // link to `/bluebey-studio/`. Vite's dev server serves `/bluebey-studio/index.html` // but 404s on the directory form, so the link appears broken in `astro dev`. // This serve-only middleware rewrites such a URL to the index file it points at, // leaving the production output untouched. function publicDirIndexPlugin() { const publicDir = fileURLToPath(new URL("./public/", import.meta.url)); return { name: "serve-public-dir-index", apply: "serve", configureServer(server) { server.middlewares.use((req, res, next) => { const [pathname, query] = (req.url ?? "").split("?", 2); // The studio is plain static files whose names never change, so a cached // copy hides edits during development (the browser keeps running the old // JS after it has been rebuilt). if (pathname.startsWith("/bluebey-studio/")) { res.setHeader("Cache-Control", "no-store"); } // Only directory requests (trailing slash) can need an index file. if (!pathname.endsWith("/")) return next(); let indexFile; try { indexFile = join(publicDir, decodeURIComponent(pathname), "index.html"); } catch { return next(); // malformed percent-encoding } // Make sure the path stayed inside public/ and the file really exists. if (!indexFile.startsWith(publicDir) || !existsSync(indexFile)) return next(); req.url = `${pathname}index.html${query ? `?${query}` : ""}`; next(); }); }, }; } // ページの最終更新日を 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: "/", trailingSlash: "always", compressHTML: true, image: { layout: "constrained", responsiveStyles: true, }, markdown: { processor: unified({ remarkPlugins: [remarkMermaid] }), }, vite: { plugins: [publicDirIndexPlugin()], }, integrations: [ sitemap({ serialize(item) { const lastmod = lastmodFor(item.url); if (lastmod) item.lastmod = lastmod; return item; }, }), updatesIntegration(), ogImagePlugin(), starlight({ title: "小平市議 安竹洋平 公式サイト", description: "一般質問や議会での活動、いじめ、発達関連の情報をまとめています。", defaultLocale: "root", locales: { root: { label: "日本語", lang: "ja", }, }, social: [ { icon: "x.com", label: "X", href: "https://twitter.com/yasutakeyohei", }, { icon: "code-branch", label: "CGit", href: "https://cgit.yasutakeyohei.com/", }, ], sidebar: [ { label: "ホーム", link: "/" }, { label: "実績", collapsed: true, items: [ { label: "概要", link: "/jisseki/" }, { label: "公文書管理の不正追及の軌跡", link: "/koubunsyo-kanri/", }, { label: "いじめ重大事態への対応の軌跡", collapsed: true, items: [ { label: "軌跡", link: "/ijime-judai-jitai/" }, { label: "重大事態対応フロー図", link: "/ijime-judai-jitai/flow/", }, ], }, { label: "障害者福祉施設における虐待通報対応の軌跡", link: "/fukushi-shisetsu-gyakutai/", }, { label: "合気公園の軌跡", link: "/aiki-kouen/" }, { label: "情緒固定級の軌跡", link: "/joutyo-koteikyu/" }, { label: "過剰な制限緩和の軌跡", link: "/kajo-seigen-kanwa/" }, { label: "東京サレジオ学園北側開発問題の軌跡", link: "/saresio-kaihatu/", }, { label: "ワクチン副反応救済制度の適正化の軌跡", link: "/vaccine-kyuusai-tekiseika/", }, { label: "ディスレクシア(読み書き障害)対応の軌跡", link: "/dislexia-taiou/", }, { label: "湖南衛生組合の議員報酬・給与見直しの軌跡", link: "/konan-eisei-kumiai/", }, ], }, { label: "一般質問", collapsed: true, items: [ { label: "一覧", link: "/ippan-situmon/" }, year("r8d", "令和8年度", ["9gatu", "6gatu"]), year("r7d", "令和7年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r6d", "令和6年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r5d", "令和5年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r4d", "令和4年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r3d", "令和3年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r2d", "令和2年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), year("r1d", "令和元年度", ["3gatu", "12gatu", "9gatu", "6gatu"]), ], }, { label: "委員会・請願", collapsed: true, items: [ { label: "一覧", link: "/iinkai/" }, { label: "令和8年度", collapsed: true, items: [ { label: "請願第18号(令和8年5月8日 生活文教委員会)", link: "/iinkai/r8d/2-seigan18/", }, { label: "請願第19号(令和8年8月26日 厚生委員会)", link: "/iinkai/r8d/1-seigan19/", }, ], }, { label: "令和6年度", collapsed: true, items: [ { label: "請願第7号(令和6年6月12日 生活文教委員会)", link: "/iinkai/r6d/1-seigan7/", }, { label: "請願第11号(令和6年9月11日 生活文教委員会)", link: "/iinkai/r6d/2-seigan11/", }, { label: "請願第12号(令和6年9月11日 生活文教委員会)", link: "/iinkai/r6d/3-seigan12/", }, { label: "請願第13号(請願第11号を出し直し)", link: "/iinkai/r6d/4-seigan13/", }, { label: "請願第14号(請願第12号を出し直し)", link: "/iinkai/r6d/5-seigan14/", }, ], }, { label: "令和5年度", collapsed: true, items: [ { label: "請願第1号(令和5年8月22日 生活文教委員会)", link: "/iinkai/r5d/1-seigan1/", }, ], }, { label: "令和3年度", collapsed: true, items: [ { label: "請願第12号(令和4年2月2日 生活文教委員会)", link: "/iinkai/r3d/1-seigan12/", }, { label: "請願第8号(令和3年9月14日 生活文教委員会)", link: "/iinkai/r3d/2-seigan8/", }, ], }, { label: "令和2年度", collapsed: true, items: [ { label: "請願第5号(令和3年3月9日 総務委員会)", link: "/iinkai/r2d/1-seigan5/", }, ], }, ], }, { label: "メディア掲載", link: "/media/" }, { label: "ディスレクシア", collapsed: true, items: [ { label: "概要", link: "/about-dyslexia/" }, { label: "ディスレクシアとは", link: "/about-dyslexia/what-is-dyslexia/", }, { label: "ディスレクシアの著名人", link: "/about-dyslexia/celebrities/", }, { label: "本人や保護者の声", link: "/about-dyslexia/voices/", }, { label: "潜在的人数と文科省調査", link: "/about-dyslexia/potential-number/", }, { label: "見過ごさないためのアセスメント", link: "/about-dyslexia/assessment/", }, { label: "デイジー教科書と音声教材", link: "/about-dyslexia/daisy-and-onsei/", }, { label: "合理的配慮と関連法律", link: "/about-dyslexia/reasonable-accommodation/", }, { label: "小平市の状況", link: "/about-dyslexia/kodaira/", }, { label: "就学相談・通級指導", link: "/about-dyslexia/school-consultation/", }, { label: "情緒固定級とは", link: "/about-dyslexia/emotional-support-class/", }, { label: "チャレンジスクール等", link: "/about-dyslexia/high-school-options/", }, { label: "長期欠席支援シート", link: "/about-dyslexia/support-sheet/", }, { label: "支援リソース・参考情報", link: "/about-dyslexia/support-resources/", }, { label: "書籍・マンガ・映画", link: "/about-dyslexia/books/", }, ], }, { label: "活動のツール", collapsed: true, items: [ { label: "Whisperで文字起こしする方法", link: "/whisper-to-ai-moji-okoshi/", }, { label: "textlintで文章校正する方法", link: "/textlint-guide/", }, ], }, { label: "小平市のデータ", collapsed: true, items: [ { label: "小平市の公園一覧", link: "/kodaira-data/parks/" }, { label: "小平市ホームページのアーカイブ(暫定)とお問い合わせ先(電話番号)一覧", link: "/kodaira-data/hp-archive/", }, ], }, { label: "ご支援", link: "/support/" }, { label: "コンタクト", link: "/contact/" }, { label: "私の方針", link: "/policy/" }, ], customCss: ["./src/styles/custom.css"], tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 6 }, lastUpdated: true, editLink: { baseUrl: "https://cgit.yasutakeyohei.com/yasutakeyohei.com.git/log/", }, plugins: [], pagefind: true, head: [ { tag: "link", attrs: { rel: "icon", href: "/favicon.ico", type: "image/x-icon", }, }, { tag: "script", attrs: { defer: true, src: "https://umami.yasutakeyohei.com/script.js", "data-website-id": "75200d05-d977-4664-83f4-148e2a4f774b", }, }, { tag: "meta", attrs: { property: "og:image", content: "https://yasutakeyohei.com/og/index.jpg?v=2", }, }, { tag: "meta", attrs: { name: "twitter:card", content: "summary_large_image", }, }, { tag: "meta", attrs: { name: "twitter:image", content: "https://yasutakeyohei.com/og/index.jpg?v=2", }, }, { tag: "script", attrs: { type: "application/ld+json" }, content: JSON.stringify({ "@context": "https://schema.org", "@type": "Person", name: "安竹洋平", givenName: "洋平", familyName: "安竹", jobTitle: "小平市議会議員", affiliation: { "@type": "Organization", name: "小平市議会", address: { "@type": "PostalAddress", addressLocality: "小平市", addressRegion: "東京都", addressCountry: "JP", }, }, image: "https://yasutakeyohei.com/img/faceicon.jpg", url: "https://yasutakeyohei.com/", sameAs: ["https://twitter.com/yasutakeyohei"], }), }, { tag: "script", content: "document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.sl-markdown-content h2[id], .sl-markdown-content h3[id]').forEach(h => { const a = document.createElement('a'); a.href = '#' + h.id; a.className = 'heading-anchor'; a.innerHTML = ''; a.title = 'クリックでこの見出しのURLをコピー'; a.addEventListener('click', e => { e.preventDefault(); const url = location.origin + location.pathname + '#' + h.id; navigator.clipboard.writeText(url).then(() => { a.classList.add('copied'); a.title = 'コピーしました!'; setTimeout(() => { a.classList.remove('copied'); a.title = 'クリックでこの見出しのURLをコピー'; }, 2000); }); }); h.prepend(a); }); });", }, { tag: "script", content: "document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('[href=\"#_top\"] span').forEach(el => { if (el.textContent === 'トップへ戻る') el.textContent = ''; }); });", }, ], components: { PageTitle: "./src/components/starlight/PageTitle.astro", SocialIcons: "./src/components/starlight/SocialIcons.astro", Footer: "./src/components/starlight/Footer.astro", }, }), ], }); function year(dir, label, months) { const MONTHS = { "6gatu": "6月定例会", "9gatu": "9月定例会", "12gatu": "12月定例会", "3gatu": "3月定例会", }; const items = []; items.push({ label: "一覧", link: `ippan-situmon/${dir}/` }); for (const m of months) { items.push({ label: MONTHS[m], collapsed: true, items: [{ autogenerate: { directory: `ippan-situmon/${dir}/${m}` } }], }); } return { label, collapsed: true, items }; }