aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/ScheduleSeo.astro
blob: b87ec5bca8999c984d47a8c6550162dd2e39714f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
---
// このページの構造化データ(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();
const updated = 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())}:00${offsetSign}${pad(Math.floor(offsetAbs / 60))}:${pad(offsetAbs % 60)}`;

const todayKey = iso;

// 構造化データに入れるのは、ビルド日から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',
  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',
        },
      },
    },
  })),
};
---

<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>
<p class="schedule-cache-note">
  新しい情報が表示されないときは、ページが保存(キャッシュ)されている可能性があります。
  ブラウザで再読み込み(Windowsは Ctrl+F5、Macは Command+Shift+R など)をお試しください。
</p>