--- // このページの構造化データ(JSON-LD)と「最終更新」。 // 更新日は、ページ本体と予定データのうち最後にコミットされた時刻を使う。 // (ビルド時刻を使うと、内容が変わっていなくても日付が動いてしまうため) import { execSync } from 'node:child_process'; import { scheduleAbsoluteUrl } from '@/data/schedule/config'; import events from '@/data/schedule/events.json'; import { entryDates } from '@/lib/schedule-dates'; import { SCHEDULE_PAGE_FILES } from '@/data/schedule/content-files.mjs'; // URLは src/data/schedule/config.ts で一元管理している(ページ移転時はそこだけ変える) const url = scheduleAbsoluteUrl(); function lastCommitDate(): Date { try { const out = execSync(`git log -1 --format=%cI -- ${SCHEDULE_PAGE_FILES.join(' ')}`, { encoding: 'utf-8', }).trim(); if (out) return new Date(out); } catch { // git が使えない環境ではビルド時刻にする } return new Date(); } const updated = lastCommitDate(); const now = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); // 表示は日本時間。dateModified はタイムゾーン付きで出す。 const year = updated.getFullYear(); const month = updated.getMonth() + 1; const day = updated.getDate(); const iso = `${year}-${pad(month)}-${pad(day)}`; const label = `${year}年${month}月${day}日 ${pad(updated.getHours())}:${pad(updated.getMinutes())}`; const offsetMinutes = -updated.getTimezoneOffset(); const offsetSign = offsetMinutes >= 0 ? '+' : '-'; const offsetAbs = Math.abs(offsetMinutes); const isoDateTime = `${iso}T${pad(updated.getHours())}:${pad(updated.getMinutes())}:${pad(updated.getSeconds())}${offsetSign}${pad(Math.floor(offsetAbs / 60))}:${pad(offsetAbs % 60)}`; // 「これからの予定」は、ビルドした日を基準にする。 const todayKey = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; // 構造化データに入れるのは、ビルド日から1週間先までの予定(近い予定に絞る) const until = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7); const untilKey = `${until.getFullYear()}-${pad(until.getMonth() + 1)}-${pad(until.getDate())}`; const stripTags = (value: string) => value .replace(/<[^>]*>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, ' ') .trim(); // タイトル先頭のアイコン(絵文字)を落とす const plainName = (title: string) => stripTags(title).replace(/^[^\p{L}\p{N}]+/u, '').trim(); // 「午前9時から/市役所2階」のようなセルから、時間と会場を取り出す。 // 「/」がない場合は、時刻らしい文字が入っているかを手がかりにする。 const timeOf = (place: string) => { const first = stripTags(place).split('/')[0].trim(); return /時|正午|から/.test(first) ? first : ''; }; const venueOf = (place: string) => { const text = stripTags(place); const parts = text.split('/'); if (parts.length > 1) return parts[1].trim(); return /時|から|まで|予約|配布|配信|申込|[0-90-9]/.test(text) ? '' : text.trim(); }; type Row = { date: string; title: string; place: string; note: string }; // ビルド日以降に開催されるイベントを、開催日の早い順に出力する。 // (ブラウザで組み立てるとGoogleが拾いにくいため、HTMLに直接入れる) const upcoming = (events.rows as Row[]) .map((row) => ({ row, dates: entryDates('event', row.date, row.place).filter( (date) => date >= todayKey && date <= untilKey, ), })) .filter(({ dates }) => dates.length > 0) .sort((a, b) => a.dates[0].localeCompare(b.dates[0])) .slice(0, 60); const data = { '@context': 'https://schema.org', '@type': 'CollectionPage', name: '小平市のイベント・行事予定カレンダー(審議会・市議会の日程も掲載)', description: '小平市のイベント・行事予定を月ごとのカレンダーで見られます。審議会・市議会の日程・会場・申込期限を一覧にし、カテゴリ別の絞り込みと地図で確認できます。', url, inLanguage: 'ja', dateModified: isoDateTime, about: [ { '@type': 'Thing', name: '小平市のイベントカレンダー' }, { '@type': 'Thing', name: '小平市の審議会・市議会の日程' }, { '@type': 'Thing', name: '小平市議会の傍聴' }, ], isPartOf: { '@type': 'WebSite', name: '小平市議 安竹洋平 公式サイト', url: 'https://yasutakeyohei.com/', }, }; const eventList = { '@context': 'https://schema.org', '@type': 'ItemList', name: '小平市のこれからのイベント', numberOfItems: upcoming.length, itemListElement: upcoming.map(({ row, dates }, index) => ({ '@type': 'ListItem', position: index + 1, item: { '@type': 'Event', name: plainName(row.title), startDate: dates[0], eventStatus: 'https://schema.org/EventScheduled', eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode', location: { '@type': 'Place', name: venueOf(row.place) || '小平市', address: { '@type': 'PostalAddress', addressLocality: '東京都小平市', addressCountry: 'JP', }, }, }, })), }; ---