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 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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 1x 250x 250x 5x 1x 4x 4x 4x 5x 5x 5x 5x 5x 5x 5x 5x 30x 5x 250x 250x 250x 250x 250x 3178x 250x 5x 5x | /**
* @module Infrastructure/Rss/Render/Feed
* @category Intelligence Operations / Supporting Infrastructure
* @name RSS 2.0 feed builder
*
* @description
* Pure string builder for the full `rss.xml` document — channel header,
* categories, image, atom:link self-reference, and one `<item>` per
* article (with hreflang `atom:link` extensions for every alternate
* language). The lastBuildDate / pubDate / copyright year are derived
* from the most recent article so output is deterministic.
*
* Round-6 split: extracted from `scripts/generate-rss.ts`.
*
* @author Hack23 AB (Infrastructure Team)
* @license Apache-2.0
*/
import type { Language } from '../../types/language.js';
import { getRssArticles } from '../scanner.js';
import { escapeXml } from '../escape.js';
import { hreflangCode } from '../hreflang.js';
import { getBySubfolder } from '../../render-lib/article-types.js';
import { LANGUAGE_META } from '../../sitemap-html/i18n.js';
const BASE_URL = 'https://riksdagsmonitor.com';
/**
* Derive the article-type subfolder slug from a baseSlug like
* `2026-04-23-propositions` → `propositions`.
*/
function subfolderFromBaseSlug(baseSlug: string): string | null {
const m = baseSlug.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
return m ? m[1]! : null;
}
/** Localized channel title/description/self-href for one feed language. */
interface ChannelStrings {
title: string;
description: string;
language: string;
selfHref: string;
}
/**
* Resolve the localized channel strings for a feed language. English
* keeps its established branded title/description verbatim so the legacy
* `rss.xml` output is byte-stable; every other language reuses the
* localized strings already maintained in `LANGUAGE_META`.
*/
function channelStrings(feedLang: Language): ChannelStrings {
if (feedLang === 'en') {
return {
title: 'Riksdagsmonitor - Swedish Parliament Intelligence',
description:
'Real-time monitoring, analysis, and intelligence from the Swedish Parliament (Riksdag) and Government. Covering legislative activity, voting patterns, coalition dynamics, and election forecasts.',
language: 'en',
selfHref: `${BASE_URL}/rss.xml`,
};
}
const meta = LANGUAGE_META[feedLang];
const t = meta.translations;
return {
title: `Riksdagsmonitor — ${t.newsAnalysis} (${meta.nativeName})`,
description: t.newsDesc,
language: meta.hreflang,
selfHref: `${BASE_URL}/rss_${feedLang}.xml`,
};
}
/**
* Generate RSS 2.0 XML feed for the requested `feedLang` (defaults to
* English). The channel header is localized via `LANGUAGE_META` and each
* item carries the localized title/description of its language variant.
*/
export function generateRss(feedLang: Language = 'en'): string {
console.log(`🔨 Generating RSS feed (${feedLang})...`);
const channel = channelStrings(feedLang);
const articles = getRssArticles(feedLang);
const now = new Date().toUTCString();
const lastBuildDate = articles.length > 0
? new Date(articles[0]!.pubDate).toUTCString()
: now;
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>${escapeXml(channel.title)}</title>
<link>${BASE_URL}</link>
<description>${escapeXml(channel.description)}</description>
<language>${channel.language}</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<pubDate>${lastBuildDate}</pubDate>
<ttl>60</ttl>
<copyright>Copyright ${articles.length > 0 ? new Date(articles[0]!.pubDate).getUTCFullYear() : new Date(lastBuildDate).getUTCFullYear()} Hack23 AB. Licensed under Apache-2.0.</copyright>
<managingEditor>info@hack23.com (Hack23 AB)</managingEditor>
<webMaster>info@hack23.com (Hack23 AB)</webMaster>
<generator>Riksdagsmonitor RSS Generator v1.0</generator>
<docs>https://www.rssboard.org/rss-specification</docs>
<image>
<url>https://riksdagsmonitor.com/images/android-chrome-512x512.png</url>
<title>Riksdagsmonitor</title>
<link>${BASE_URL}</link>
<width>144</width>
<height>144</height>
<description>Riksdagsmonitor - Swedish Parliament Intelligence Platform</description>
</image>
<atom:link href="${channel.selfHref}" rel="self" type="application/rss+xml"/>`;
const channelCategories = [
'Swedish Politics', 'Parliament', 'Riksdag', 'Political Intelligence',
'Election Analysis', 'Legislative Monitoring',
];
for (const cat of channelCategories) {
xml += `
<category>${escapeXml(cat)}</category>`;
}
for (const article of articles) {
const subfolder = subfolderFromBaseSlug(article.baseSlug);
const typeEntry = subfolder ? getBySubfolder(subfolder) : undefined;
const categoryLabel = typeEntry
? `${typeEntry.icon} ${typeEntry.label}`
: article.category;
xml += `
<item>
<title>${escapeXml(article.title)}</title>
<link>${escapeXml(article.link)}</link>
<description>${escapeXml(article.description)}</description>
<pubDate>${new Date(article.pubDate).toUTCString()}</pubDate>
<guid isPermaLink="true">${escapeXml(article.link)}</guid>
<dc:creator>${escapeXml(article.author)}</dc:creator>
<category>${escapeXml(categoryLabel)}</category>
<atom:link href="${escapeXml(article.link)}" rel="alternate" type="text/html" hreflang="${hreflangCode(feedLang)}"/>`;
for (const alt of article.alternateLanguages) {
xml += `
<atom:link href="${escapeXml(alt.href)}" rel="alternate" type="text/html" hreflang="${hreflangCode(alt.lang)}"/>`;
}
xml += `
</item>`;
}
xml += `
</channel>
</rss>`;
return xml;
}
|