aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/ScheduleMonthLinks.astro32
-rw-r--r--src/content/docs/kodaira/events.mdx11
-rw-r--r--src/lib/schedule-months.ts34
-rw-r--r--src/pages/kodaira/events/[month].astro72
-rw-r--r--src/pages/kodaira/events/[period].astro124
-rw-r--r--src/pages/kodaira/events/feed.xml.ts79
-rw-r--r--src/styles/custom.css16
7 files changed, 289 insertions, 79 deletions
diff --git a/src/components/ScheduleMonthLinks.astro b/src/components/ScheduleMonthLinks.astro
index 8628998..9f553df 100644
--- a/src/components/ScheduleMonthLinks.astro
+++ b/src/components/ScheduleMonthLinks.astro
@@ -1,15 +1,35 @@
---
-// 月別アーカイブのリンク一覧。行事予定ページの下部に置く。
-import { monthCounts, monthKeys, monthLabel } from '@/lib/schedule-months';
+// 月別・年度別アーカイブのリンク一覧。行事予定ページの下部に置く。
+import {
+ fiscalYearKeys,
+ fiscalYearLabel,
+ fiscalYearOf,
+ monthCounts,
+ monthKeys,
+ monthLabel,
+} from '@/lib/schedule-months';
import { scheduleUrl } from '@/data/schedule/config';
+
+const groups = fiscalYearKeys.map((fy) => ({
+ key: fy,
+ label: fiscalYearLabel(fy),
+ months: monthKeys.filter((month) => fiscalYearOf(month) === fy),
+}));
---
<ul class="schedule-month-archive">
{
- monthKeys.map((key) => (
- <li>
- <a href={scheduleUrl(`${key}/`)}>{monthLabel(key)}の予定</a>
- <span class="schedule-month-count">{monthCounts[key]}件</span>
+ groups.map((group) => (
+ <li class="schedule-year-archive">
+ <a href={scheduleUrl(`${group.key}/`)}>{group.label}の予定</a>
+ <ul>
+ {group.months.map((key) => (
+ <li>
+ <a href={scheduleUrl(`${key}/`)}>{monthLabel(key)}の予定</a>
+ <span class="schedule-month-count">{monthCounts[key]}件</span>
+ </li>
+ ))}
+ </ul>
</li>
))
}
diff --git a/src/content/docs/kodaira/events.mdx b/src/content/docs/kodaira/events.mdx
index 3455427..b3636bc 100644
--- a/src/content/docs/kodaira/events.mdx
+++ b/src/content/docs/kodaira/events.mdx
@@ -2,6 +2,13 @@
title: 小平市のイベント・行事予定カレンダー(審議会・市議会の日程も掲載)
description: 小平市のイベント・行事予定を月ごとのカレンダーで見られます。審議会・市議会の日程・会場・申込期限を一覧にし、カテゴリ別の絞り込みと地図で確認できます。
tableOfContents: true
+head:
+ - tag: link
+ attrs:
+ rel: alternate
+ type: application/rss+xml
+ title: 小平市のイベント・行事予定(RSS)
+ href: /kodaira/events/feed.xml
---
import ScheduleInteractive from '../../../components/ScheduleInteractive.astro';
@@ -90,10 +97,12 @@ import ScheduleMonthLinks from '../../../components/ScheduleMonthLinks.astro';
## 月別のアーカイブ
-過去の月の予定は、月ごとのページで見られます。
+過去の月の予定は、月ごとのページで見られます。年度ごとの一覧もあります。
<ScheduleMonthLinks />
+これからの予定の更新は、[RSS](/kodaira/events/feed.xml)でも受け取れます。
+
## ご注意
イベントの内容・日程は変更になることがあります。最新の情報は、各問い合わせ先または市公式Xでご確認ください。
diff --git a/src/lib/schedule-months.ts b/src/lib/schedule-months.ts
index cd109b7..829fef5 100644
--- a/src/lib/schedule-months.ts
+++ b/src/lib/schedule-months.ts
@@ -42,3 +42,37 @@ export function monthLabel(key: string): string {
const [y, m] = key.split('-').map(Number);
return `${y}年${m}月`;
}
+
+/** 月キー → 年度キー(令和8年度は r8。4月〜翌年3月) */
+export function fiscalYearOf(month: string): string {
+ const [y, m] = month.split('-').map(Number);
+ const startYear = m >= 4 ? y : y - 1;
+ return `r${startYear - 2018}`;
+}
+
+/** データに存在する年度(昇順) */
+export const fiscalYearKeys: string[] = [...new Set(monthKeys.map(fiscalYearOf))].sort();
+
+/** 年度に含まれる月キー(データにある月だけ) */
+export function monthsInFiscalYear(fy: string): string[] {
+ return monthKeys.filter((month) => fiscalYearOf(month) === fy);
+}
+
+/** 年度の表示(r8 → 令和8年度) */
+export function fiscalYearLabel(fy: string): string {
+ return `令和${Number(fy.slice(1))}年度`;
+}
+
+/** 複数の月のいずれかに開催される行を返す */
+export function rowsInMonths(months: string[], type: 'meeting' | 'event'): Row[] {
+ const inMonths = (item: Item) =>
+ datesOf(item).some((date) => months.includes(date.slice(0, 7)));
+ return items
+ .filter((item) => item.type === type && inMonths(item))
+ .map((item) => item.row);
+}
+
+/** 年度に開催される行を返す */
+export function rowsInFiscalYear(fy: string, type: 'meeting' | 'event'): Row[] {
+ return rowsInMonths(monthsInFiscalYear(fy), type);
+}
diff --git a/src/pages/kodaira/events/[month].astro b/src/pages/kodaira/events/[month].astro
deleted file mode 100644
index 88ce61f..0000000
--- a/src/pages/kodaira/events/[month].astro
+++ /dev/null
@@ -1,72 +0,0 @@
----
-// 月別アーカイブ(/kodaira/events/2026-11/ など)。
-// 行事予定のデータ(src/data/schedule/*.json)から、その月の予定だけを静的に生成する。
-import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
-import ScheduleTable from '@/components/ScheduleTable.astro';
-import { monthKeys, monthLabel, rowsInMonth } from '@/lib/schedule-months';
-import { SCHEDULE_BASE_PATH } from '@/data/schedule/config';
-
-export function getStaticPaths() {
- return monthKeys.map((month, i) => ({
- params: { month },
- props: {
- month,
- prev: monthKeys[i - 1] ?? null,
- next: monthKeys[i + 1] ?? null,
- },
- }));
-}
-
-const { month, prev, next } = Astro.props as {
- month: string;
- prev: string | null;
- next: string | null;
-};
-
-const label = monthLabel(month);
-const meetingRows = rowsInMonth(month, 'meeting');
-const eventRows = rowsInMonth(month, 'event');
-
-const url = (key: string) => `${SCHEDULE_BASE_PATH}${key}/`;
----
-
-<StarlightPage
- frontmatter={{
- title: `小平市のイベント・行事予定(${label})`,
- description: `${label}の小平市のイベントと、審議会・市議会の開催予定の一覧です。日程・会場・申込期限を確認できます。`,
- }}
->
- <p><a href={SCHEDULE_BASE_PATH}>← 小平市のイベント・行事予定カレンダー(今後)に戻る</a></p>
-
- <p>
- {label}の小平市のイベントと、審議会等・市議会の開催予定です。開催日が過ぎたものも記録として掲載しています。
- 変更になることがありますので、各問い合わせ先へご確認ください。
- </p>
-
- {
- meetingRows.length > 0 && (
- <>
- <h2>審議会等・市議会の開催予定</h2>
- <div class="schedule-table schedule-table--date-nowrap">
- <ScheduleTable variant="meeting" rows={meetingRows} />
- </div>
- </>
- )
- }
-
- {
- eventRows.length > 0 && (
- <>
- <h2>イベント</h2>
- <div class="schedule-table">
- <ScheduleTable variant="event" rows={eventRows} />
- </div>
- </>
- )
- }
-
- <nav class="schedule-month-nav" aria-label="月の移動">
- {prev ? <a href={url(prev)}>← {monthLabel(prev)}</a> : <span />}
- {next ? <a href={url(next)}>{monthLabel(next)} →</a> : <span />}
- </nav>
-</StarlightPage>
diff --git a/src/pages/kodaira/events/[period].astro b/src/pages/kodaira/events/[period].astro
new file mode 100644
index 0000000..7ef4599
--- /dev/null
+++ b/src/pages/kodaira/events/[period].astro
@@ -0,0 +1,124 @@
+---
+// 月別・年度別アーカイブ(/kodaira/events/2026-11/、/kodaira/events/r8/ など)。
+// 行事予定のデータ(src/data/schedule/*.json)から、その期間の予定だけを静的に生成する。
+import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
+import ScheduleTable from '@/components/ScheduleTable.astro';
+import {
+ fiscalYearKeys,
+ fiscalYearLabel,
+ fiscalYearOf,
+ monthCounts,
+ monthKeys,
+ monthLabel,
+ monthsInFiscalYear,
+ rowsInFiscalYear,
+ rowsInMonth,
+} from '@/lib/schedule-months';
+import { SCHEDULE_BASE_PATH } from '@/data/schedule/config';
+
+export function getStaticPaths() {
+ return [
+ ...monthKeys.map((key, i) => ({
+ params: { period: key },
+ props: {
+ kind: 'month' as const,
+ key,
+ prev: monthKeys[i - 1] ?? null,
+ next: monthKeys[i + 1] ?? null,
+ },
+ })),
+ ...fiscalYearKeys.map((key, i) => ({
+ params: { period: key },
+ props: {
+ kind: 'fiscal' as const,
+ key,
+ prev: fiscalYearKeys[i - 1] ?? null,
+ next: fiscalYearKeys[i + 1] ?? null,
+ },
+ })),
+ ];
+}
+
+const { kind, key, prev, next } = Astro.props as {
+ kind: 'month' | 'fiscal';
+ key: string;
+ prev: string | null;
+ next: string | null;
+};
+
+const label = kind === 'fiscal' ? fiscalYearLabel(key) : monthLabel(key);
+const periodLabel = (period: string) =>
+ kind === 'fiscal' ? fiscalYearLabel(period) : monthLabel(period);
+
+const meetingRows =
+ kind === 'fiscal' ? rowsInFiscalYear(key, 'meeting') : rowsInMonth(key, 'meeting');
+const eventRows =
+ kind === 'fiscal' ? rowsInFiscalYear(key, 'event') : rowsInMonth(key, 'event');
+
+// 年度ページでは、その年度の月別ページへの一覧も並べる。
+const months = kind === 'fiscal' ? monthsInFiscalYear(key) : [];
+
+const url = (period: string) => `${SCHEDULE_BASE_PATH}${period}/`;
+---
+
+<StarlightPage
+ frontmatter={{
+ title: `小平市のイベント・行事予定(${label})`,
+ description: `${label}の小平市のイベントと、審議会・市議会の開催予定の一覧です。日程・会場・申込期限を確認できます。`,
+ }}
+>
+ <p><a href={SCHEDULE_BASE_PATH}>← 小平市のイベント・行事予定カレンダー(今後)に戻る</a></p>
+
+ <p>
+ {label}の小平市のイベントと、審議会等・市議会の開催予定です。開催日が過ぎたものも記録として掲載しています。
+ 変更になることがありますので、各問い合わせ先へご確認ください。
+ </p>
+
+ {
+ kind === 'fiscal' && (
+ <ul class="schedule-month-archive">
+ {months.map((month) => (
+ <li>
+ <a href={url(month)}>{monthLabel(month)}の予定</a>
+ <span class="schedule-month-count">{monthCounts[month]}件</span>
+ </li>
+ ))}
+ </ul>
+ )
+ }
+
+ {
+ meetingRows.length > 0 && (
+ <>
+ <h2>審議会等・市議会の開催予定</h2>
+ <div class="schedule-table schedule-table--date-nowrap">
+ <ScheduleTable variant="meeting" rows={meetingRows} />
+ </div>
+ </>
+ )
+ }
+
+ {
+ eventRows.length > 0 && (
+ <>
+ <h2>イベント</h2>
+ <div class="schedule-table">
+ <ScheduleTable variant="event" rows={eventRows} />
+ </div>
+ </>
+ )
+ }
+
+ {
+ kind === 'month' && (
+ <p class="schedule-fiscal-link">
+ <a href={url(fiscalYearOf(key))}>{fiscalYearLabel(fiscalYearOf(key))}の予定をまとめて見る</a>
+ </p>
+ )
+ }
+
+ <nav class="schedule-month-nav" aria-label={kind === 'fiscal' ? '年度の移動' : '月の移動'}>
+ {prev ? <a href={url(prev)}>← {periodLabel(prev)}</a> : <span />}
+ {next ? <a href={url(next)}>{periodLabel(next)} →</a> : <span />}
+ </nav>
+</StarlightPage>
diff --git a/src/pages/kodaira/events/feed.xml.ts b/src/pages/kodaira/events/feed.xml.ts
new file mode 100644
index 0000000..860f5f2
--- /dev/null
+++ b/src/pages/kodaira/events/feed.xml.ts
@@ -0,0 +1,79 @@
+// これからの予定のRSSフィード(/kodaira/events/feed.xml)。
+// ビルド時点の日付以降に開催される予定を、開催日の早い順に最大60件出力する。
+import type { APIRoute } from 'astro';
+import { items } from '@/lib/schedule-months';
+import { entryDates } from '@/lib/schedule-dates';
+import { SCHEDULE_BASE_PATH, SITE_URL } from '@/data/schedule/config';
+
+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 escapeXml = (value: string) =>
+ value
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;');
+
+const pad = (n: number) => String(n).padStart(2, '0');
+const now = new Date();
+const todayKey = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
+
+const toRfc822 = (date: string) => new Date(`${date}T00:00:00+09:00`).toUTCString();
+
+const entries = items
+ .map((item) => {
+ const dates = entryDates(item.type, item.row.date, item.row.place);
+ return { item, upcoming: dates.filter((date) => date >= todayKey) };
+ })
+ .filter(({ upcoming }) => upcoming.length > 0)
+ .sort((a, b) => a.upcoming[0].localeCompare(b.upcoming[0]))
+ .slice(0, 60);
+
+const itemXml = ({ item: { type, row }, upcoming }: (typeof entries)[number]) => {
+ const title = stripTags(row.title);
+ const description = [stripTags(row.place), stripTags(row.note)].filter(Boolean).join('/');
+ const datesLabel = upcoming
+ .map((date) => `${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日`)
+ .join('、');
+ const link = `${SITE_URL}${SCHEDULE_BASE_PATH}${upcoming[0].slice(0, 7)}/`;
+ const guid = `${SITE_URL}${SCHEDULE_BASE_PATH}#${type}-${upcoming[0]}-${encodeURIComponent(title)}`;
+
+ return ` <item>
+ <title>${escapeXml(title)}</title>
+ <link>${escapeXml(link)}</link>
+ <guid isPermaLink="false">${escapeXml(guid)}</guid>
+ <category>${type === 'meeting' ? '審議会等・市議会' : 'イベント'}</category>
+ <pubDate>${toRfc822(upcoming[0])}</pubDate>
+ <description>${escapeXml(`開催日:${datesLabel}${description ? `/${description}` : ''}`)}</description>
+ </item>`;
+};
+
+export const GET: APIRoute = () => {
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+ <channel>
+ <title>小平市のイベント・行事予定(審議会・市議会の日程も掲載)</title>
+ <link>${SITE_URL}${SCHEDULE_BASE_PATH}</link>
+ <description>小平市のイベントと、審議会等・市議会の開催予定です。市報こだいら・市公式X・LoGoフォーム・市議会の行事予定表などをもとにまとめています。</description>
+ <language>ja</language>
+ <lastBuildDate>${now.toUTCString()}</lastBuildDate>
+${entries.map(itemXml).join('\n')}
+ </channel>
+</rss>
+`;
+
+ return new Response(xml, {
+ headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
+ });
+};
diff --git a/src/styles/custom.css b/src/styles/custom.css
index 82b5c9f..668e922 100644
--- a/src/styles/custom.css
+++ b/src/styles/custom.css
@@ -2271,6 +2271,22 @@ td.sanpi-open .sanpi-title {
color: var(--sl-color-gray-3);
font-size: 0.8rem;
}
+.schedule-year-archive {
+ display: block;
+ width: 100%;
+ margin: 0 0 0.6rem;
+}
+.schedule-year-archive > a {
+ font-weight: 600;
+}
+.schedule-year-archive > ul {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem 1.2rem;
+ margin: 0.35rem 0 0;
+ padding: 0 0 0 1rem;
+ list-style: none;
+}
.schedule-month-nav {
display: flex;
justify-content: space-between;