diff options
| -rw-r--r-- | src/components/ScheduleMonthLinks.astro | 32 | ||||
| -rw-r--r-- | src/content/docs/kodaira/events.mdx | 11 | ||||
| -rw-r--r-- | src/lib/schedule-months.ts | 34 | ||||
| -rw-r--r-- | src/pages/kodaira/events/[month].astro | 72 | ||||
| -rw-r--r-- | src/pages/kodaira/events/[period].astro | 124 | ||||
| -rw-r--r-- | src/pages/kodaira/events/feed.xml.ts | 79 | ||||
| -rw-r--r-- | src/styles/custom.css | 16 |
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(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); + +const escapeXml = (value: string) => + value + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +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; |
