All files / src/generators/sitemap rss.ts

100% Statements 3/3
100% Branches 1/1
100% Functions 2/2
100% Lines 3/3

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                                                19x   213x                     19x                              
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Generators/Sitemap/Rss
 * @description Generates the `rss.xml` feed for the homepage. The feed
 * surfaces every news article across all 14 supported languages — items
 * are sorted newest-first by `pubDate` and tagged with `<dc:language>`
 * so feed readers can filter or split per locale.
 *
 * Extracted from the monolithic `sitemap.ts` so RSS generation can be
 * unit-tested in isolation and so any future feed format (Atom,
 * JSON-Feed, news-specific RSS) reuses the same item shape and escaping.
 */
 
import { BASE_URL } from '../../constants/config.js';
import { escapeXML } from './xml-utils.js';
 
/**
 * Single RSS feed entry.
 *
 * `pubDate` is expected to already be RFC-822 formatted (the same format
 * the consuming `<pubDate>` element uses). The wrapper does not reformat
 * dates so callers can opt into different cadences (per-day, per-article)
 * without surprising rounding.
 */
export interface RssItem {
  /** Article title */
  readonly title: string;
  /** Absolute article URL */
  readonly link: string;
  /** Plain-text article description */
  readonly description: string;
  /** RFC-822 publication timestamp */
  readonly pubDate: string;
  /** ISO 639-1 language code (e.g. `en`, `sv`, `de`) */
  readonly lang: string;
}
 
/**
 * Generate an RSS 2.0 XML feed for the homepage.
 *
 * Every item is escaped via {@link escapeXML} so titles or descriptions
 * containing `&`/`<`/etc. don't break feed readers. The channel-level
 * `<lastBuildDate>` reflects "now" — callers that need deterministic
 * output for tests should pass an explicit `buildDate` override.
 *
 * @param articleInfos - Articles, ideally already sorted newest-first
 * @param buildDate - Optional override for the channel-level
 *   `<lastBuildDate>` (RFC-822 string). Defaults to `new Date().toUTCString()`.
 * @returns Complete RSS 2.0 XML document
 */
export function generateRssFeed(
  articleInfos: readonly RssItem[],
  buildDate: string = new Date().toUTCString()
): string {
  const items = articleInfos
    .map(
      (item) => `    <item>
      <title>${escapeXML(item.title)}</title>
      <link>${escapeXML(item.link)}</link>
      <description>${escapeXML(item.description)}</description>
      <pubDate>${item.pubDate}</pubDate>
      <guid isPermaLink="true">${escapeXML(item.link)}</guid>
      <dc:language>${escapeXML(item.lang)}</dc:language>
    </item>`
    )
    .join('\n');
 
  return `<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>EU Parliament Monitor</title>
    <link>${BASE_URL}</link>
    <description>European Parliament Intelligence Platform — monitoring political activity with systematic transparency.</description>
    <language>en</language>
    <lastBuildDate>${buildDate}</lastBuildDate>
    <atom:link href="${BASE_URL}/rss.xml" rel="self" type="application/rss+xml"/>
${items}
  </channel>
</rss>`;
}