diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ScheduleInteractive.astro | 55 | ||||
| -rw-r--r-- | src/components/ScheduleSeo.astro | 80 |
2 files changed, 80 insertions, 55 deletions
diff --git a/src/components/ScheduleInteractive.astro b/src/components/ScheduleInteractive.astro index 8877d9d..19a3f59 100644 --- a/src/components/ScheduleInteractive.astro +++ b/src/components/ScheduleInteractive.astro @@ -1,6 +1,5 @@ <script> // URLは src/data/schedule/config.ts で一元管理している(ページ移転時はそこだけ変える) - import { scheduleAbsoluteUrl } from '@/data/schedule/config'; import { parseDates, applyExclusions, toKey, keyToDate, entryDates, mainStartMonth, normalizeDigits } from '@/lib/schedule-dates'; const CATEGORY_DEFS = [ @@ -486,59 +485,6 @@ }); } - // 検索エンジン向けに、これからのイベントを構造化データ(JSON-LD)で出力する。 - function emitEventStructuredData() { - const upcoming = entries - .filter((e) => e.type === 'event' && e.dates.some((d) => d >= todayKey)) - .map((e) => { - const future = e.dates.filter((d) => d >= todayKey).sort(); - const name = e.name.startsWith(e.category) - ? e.name.slice(e.category.length).trim() - : e.name; - const parts = e.time.split('/'); - const venue = (parts[1] ?? '').trim(); - return { - '@type': 'Event', - name, - startDate: future[0], - eventStatus: 'https://schema.org/EventScheduled', - eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode', - location: { - '@type': 'Place', - name: venue || '小平市', - address: { - '@type': 'PostalAddress', - addressLocality: '東京都小平市', - addressCountry: 'JP', - }, - }, - description: e.time, - url: scheduleAbsoluteUrl(`#${e.rowId}`), - }; - }) - .sort((a, b) => a.startDate.localeCompare(b.startDate)) - .slice(0, 40); - - const existing = document.getElementById('schedule-event-ldjson'); - if (existing) existing.remove(); - if (upcoming.length === 0) return; - - const script = document.createElement('script'); - script.type = 'application/ld+json'; - script.id = 'schedule-event-ldjson'; - script.textContent = JSON.stringify({ - '@context': 'https://schema.org', - '@type': 'ItemList', - name: '小平市のこれからのイベント', - itemListElement: upcoming.map((item, index) => ({ - '@type': 'ListItem', - position: index + 1, - item, - })), - }); - document.head.appendChild(script); - } - // 件数はフィルターのボタンには出さず、ページ下部にまとめて表示する。 function renderCategorySummary() { const summaryEl = document.getElementById('schedule-category-summary'); @@ -553,7 +499,6 @@ buildFilters(); renderCategorySummary(); - emitEventStructuredData(); applyFilter(); renderCalendar(); if (selectedDate) showDayDetails(selectedDate); diff --git a/src/components/ScheduleSeo.astro b/src/components/ScheduleSeo.astro index 03bf268..77c02a2 100644 --- a/src/components/ScheduleSeo.astro +++ b/src/components/ScheduleSeo.astro @@ -1,6 +1,8 @@ --- // このページの構造化データ(JSON-LD)。ビルド日を更新日として出力する。 import { scheduleAbsoluteUrl } from '@/data/schedule/config'; +import events from '@/data/schedule/events.json'; +import { entryDates } from '@/lib/schedule-dates'; // URLは src/data/schedule/config.ts で一元管理している(ページ移転時はそこだけ変える) const url = scheduleAbsoluteUrl(); @@ -8,6 +10,56 @@ const updated = new Date(); const iso = updated.toISOString().slice(0, 10); const label = `${updated.getFullYear()}年${updated.getMonth() + 1}月${updated.getDate()}日`; +const pad = (n: number) => String(n).padStart(2, '0'); +const todayKey = `${updated.getFullYear()}-${pad(updated.getMonth() + 1)}-${pad(updated.getDate())}`; + +// 構造化データに入れるのは、ビルド日から1週間先までの予定(近い予定に絞る) +const until = new Date(updated.getFullYear(), updated.getMonth(), updated.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', @@ -28,8 +80,36 @@ const data = { 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', + }, + }, + }, + })), +}; --- <script type="application/ld+json" set:html={JSON.stringify(data)} /> +<script type="application/ld+json" set:html={JSON.stringify(eventList)} /> <p class="schedule-updated">最終更新:{label}</p> |
