aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/pages/kodaira/events/feed.xml.ts
blob: a76b775a8fd3cd8e8e0543954d573adbe6237bbd (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
// これからの予定の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}`;
  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' },
  });
};