aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-11-06 13:35:26 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-11-06 13:35:26 +0900
commitfb5a5321822916f8ca9d0599dd6ff5599955e248 (patch)
treea125cba22e5dc4b7ed973f2b86f714d48852ea5f /src/components
parentfd171825c06456a23f34df3583cd0d4531703115 (diff)
events: イベントの構造化データをHTMLに出力
これまでJavaScriptで組み立てていたEventのJSON-LDを、ビルド時に HTMLへ出すようにした。対象はビルド日から1週間先までの予定で、 名前・開催日・会場を出力する。クライアント側の生成は重複するため外した。
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ScheduleInteractive.astro55
-rw-r--r--src/components/ScheduleSeo.astro80
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(/&nbsp;/g, ' ')
+ .replace(/&amp;/g, '&')
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"')
+ .replace(/&#39;/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>