diff --git a/website/news/app.js b/website/news/app.js
new file mode 100644
index 0000000..712bcec
--- /dev/null
+++ b/website/news/app.js
@@ -0,0 +1,280 @@
+/* app.js - fetch only when Refresh is pressed */
+
+(function () {
+ function $(id) { return document.getElementById(id); }
+
+ function parseQuery() {
+ var out = {};
+ var qs = window.location.search || "";
+ if (qs.indexOf("?") === 0) qs = qs.substring(1);
+ if (!qs) return out;
+
+ var parts = qs.split("&");
+ for (var i = 0; i < parts.length; i++) {
+ var kv = parts[i].split("=");
+ var k = decodeURIComponent(kv[0] || "").trim();
+ var v = decodeURIComponent(kv.slice(1).join("=") || "").trim();
+ if (k) out[k] = v;
+ }
+ return out;
+ }
+
+ function setQuery(params) {
+ var keys = [];
+ for (var k in params) {
+ if (params.hasOwnProperty(k) && params[k] !== "" && params[k] != null) {
+ keys.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
+ }
+ }
+ var qs = keys.length ? ("?" + keys.join("&")) : "";
+ if (window.history && window.history.replaceState) {
+ window.history.replaceState(null, "", window.location.pathname + qs);
+ }
+ }
+
+ function findById(arr, id, fallback) {
+ for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return arr[i];
+ return fallback || null;
+ }
+
+ function buildFeedUrl(feed, region) {
+ var base;
+ if (feed.type === "top") {
+ base = "https://news.google.com/rss";
+ } else if (feed.type === "topic" && feed.topic) {
+ base = "https://news.google.com/rss/headlines/section/topic/" + encodeURIComponent(feed.topic);
+ } else {
+ base = "https://news.google.com/rss";
+ }
+
+ var q = [];
+ q.push("hl=" + encodeURIComponent(region.hl));
+ q.push("gl=" + encodeURIComponent(region.gl));
+ q.push("ceid=" + encodeURIComponent(region.ceid));
+ return base + "?" + q.join("&");
+ }
+
+ function fetchText(url, cb) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.timeout = 15000;
+
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState !== 4) return;
+ if (xhr.status >= 200 && xhr.status < 300) return cb(null, xhr.responseText);
+ cb(new Error("HTTP " + xhr.status + " while fetching feed"));
+ };
+
+ xhr.ontimeout = function () {
+ cb(new Error("Timed out while fetching feed"));
+ };
+
+ xhr.onerror = function () {
+ cb(new Error("Network error while fetching feed"));
+ };
+
+ xhr.send(null);
+ }
+
+ function safeText(s) {
+ if (s == null) return "";
+ return String(s);
+ }
+
+ function escapeHtml(s) {
+ s = safeText(s);
+ return s
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function escapeAttr(s) {
+ return escapeHtml(s);
+ }
+
+ function stripHtml(s) {
+ return safeText(s).replace(/<[^>]*>/g, "");
+ }
+
+ function parseRss(xmlText) {
+ var items = [];
+ var doc = null;
+
+ if (window.DOMParser) {
+ try {
+ doc = new DOMParser().parseFromString(xmlText, "text/xml");
+ } catch (e) {
+ doc = null;
+ }
+ }
+ if (!doc || !doc.getElementsByTagName) return items;
+
+ var nodes = doc.getElementsByTagName("item");
+ for (var i = 0; i < nodes.length; i++) {
+ var it = nodes[i];
+
+ function get(tag) {
+ var n = it.getElementsByTagName(tag);
+ if (!n || !n[0] || !n[0].textContent) return "";
+ return n[0].textContent;
+ }
+
+ var title = get("title");
+ var link = get("link");
+ var pubDate = get("pubDate");
+ var desc = get("description");
+
+ var source = "";
+ if (title.indexOf(" - ") !== -1) {
+ var parts = title.split(" - ");
+ source = parts[parts.length - 1];
+ }
+
+ items.push({
+ title: title,
+ link: link,
+ pubDate: pubDate,
+ source: source,
+ description: desc
+ });
+ }
+ return items;
+ }
+
+ function showError(msg, show) {
+ var el = $("error");
+ if (!el) return;
+ if (show) {
+ el.style.display = "block";
+ el.textContent = msg;
+ } else {
+ el.style.display = "none";
+ el.textContent = "";
+ }
+ }
+
+ function renderItems(items) {
+ var host = $("items");
+ host.innerHTML = "";
+
+ if (!items || !items.length) {
+ host.innerHTML =
+ "
No items." +
+ "
Press Refresh to load.
";
+ return;
+ }
+
+ var html = [];
+ for (var i = 0; i < items.length; i++) {
+ var it = items[i];
+
+ html.push("");
+ html.push(
+ "
" +
+ escapeHtml(it.title) +
+ ""
+ );
+
+ var meta = [];
+ if (it.source) meta.push(escapeHtml(it.source));
+ if (it.pubDate) meta.push(escapeHtml(it.pubDate));
+ if (meta.length) html.push("
" + meta.join(" · ") + "
");
+
+ var snippet = stripHtml(safeText(it.description)).replace(/\s+/g, " ").trim();
+ if (snippet.length > 220) snippet = snippet.substring(0, 220) + "...";
+ if (snippet) html.push("
" + escapeHtml(snippet) + "
");
+
+ html.push("
");
+ }
+
+ host.innerHTML = html.join("");
+ }
+
+ function buildSelectOptions(selectEl, arr, selectedId) {
+ selectEl.innerHTML = "";
+ for (var i = 0; i < arr.length; i++) {
+ var o = document.createElement("option");
+ o.value = arr[i].id;
+ o.textContent = arr[i].label;
+ if (arr[i].id === selectedId) o.selected = true;
+ selectEl.appendChild(o);
+ }
+ }
+
+ function getCurrentSelection() {
+ var feeds = window.NEWS_FEEDS || [];
+ var regions = window.NEWS_REGIONS || [];
+
+ var feedId = $("feedSelect").value;
+ var regionId = $("regionSelect").value;
+
+ var feed = findById(feeds, feedId, feeds[0] || null);
+ var region = findById(regions, regionId, regions[0] || null);
+
+ return { feed: feed, region: region };
+ }
+
+ function syncUrlToSelection() {
+ setQuery({ feed: $("feedSelect").value, region: $("regionSelect").value });
+ }
+
+ function requestNews() {
+ var sel = getCurrentSelection();
+ if (!sel.feed || !sel.region) {
+ showError("feeds.js is missing NEWS_FEEDS or NEWS_REGIONS.", true);
+ return;
+ }
+
+ syncUrlToSelection();
+ showError("", false);
+
+ var feedUrl = buildFeedUrl(sel.feed, sel.region);
+
+ var proxyBase = window.NEWS_PROXY_BASE || "";
+ var fetchUrl = proxyBase ? (proxyBase + encodeURIComponent(feedUrl)) : feedUrl;
+
+ fetchText(fetchUrl, function (err, txt) {
+ if (err) {
+ showError(
+ "Could not load feed. If this is CORS, set NEWS_PROXY_BASE in feeds.js. Details: " + err.message,
+ true
+ );
+ renderItems([]);
+ return;
+ }
+ renderItems(parseRss(txt));
+ });
+ }
+
+ window.addEventListener("DOMContentLoaded", function () {
+ var feeds = window.NEWS_FEEDS || [];
+ var regions = window.NEWS_REGIONS || [];
+
+ if (!feeds.length || !regions.length) {
+ showError("No feeds/regions configured. Check feeds.js.", true);
+ renderItems([]);
+ return;
+ }
+
+ var q = parseQuery();
+ var feedId = q.feed || window.NEWS_DEFAULT_FEED_ID || feeds[0].id;
+ var regionId = q.region || window.NEWS_DEFAULT_REGION_ID || regions[0].id;
+
+ buildSelectOptions($("feedSelect"), feeds, feedId);
+ buildSelectOptions($("regionSelect"), regions, regionId);
+
+ // Keep URL updated when selecting, but DO NOT request.
+ $("feedSelect").addEventListener("change", syncUrlToSelection);
+ $("regionSelect").addEventListener("change", syncUrlToSelection);
+
+ $("refreshBtn").addEventListener("click", requestNews);
+
+ // No auto-request on load.
+ renderItems([]);
+ syncUrlToSelection();
+ });
+ })();
+
\ No newline at end of file
diff --git a/website/news/feeds.js b/website/news/feeds.js
new file mode 100644
index 0000000..1be32b3
--- /dev/null
+++ b/website/news/feeds.js
@@ -0,0 +1,116 @@
+/* feeds.js
+ Configure feeds + regions here.
+*/
+
+(function () {
+ // CORS NOTE:
+ // Browsers usually cannot fetch https://news.google.com/rss... directly due to CORS.
+ // Default uses allorigins (raw mode). You can swap to your own proxy.
+ //
+ // Examples:
+ // "" (no proxy; only works if YOUR server adds CORS headers or proxies it)
+ // "https://api.allorigins.win/raw?url="
+ // "https://r.jina.ai/http://YOUR_HTTP_PROXY/" (if you host a proxy)
+ //
+ window.NEWS_PROXY_BASE = "https://api.allorigins.win/raw?url=";
+
+ // Feed list (adding entries here automatically adds them to the dropdown)
+ // type:
+ // - "top" -> https://news.google.com/rss?... (Top headlines)
+ // - "topic"-> https://news.google.com/rss/headlines/section/topic/{TOPIC}?... (Topic feeds)
+ window.NEWS_FEEDS = [
+ { id: "top", label: "Top", type: "top" },
+ { id: "world", label: "World", type: "topic", topic: "WORLD" },
+ { id: "nation", label: "Nation", type: "topic", topic: "NATION" },
+ { id: "business", label: "Business", type: "topic", topic: "BUSINESS" },
+ { id: "technology", label: "Technology", type: "topic", topic: "TECHNOLOGY" },
+ { id: "entertainment", label: "Entertainment", type: "topic", topic: "ENTERTAINMENT" },
+ { id: "sports", label: "Sports", type: "topic", topic: "SPORTS" },
+ { id: "science", label: "Science", type: "topic", topic: "SCIENCE" },
+ { id: "health", label: "Health", type: "topic", topic: "HEALTH" }
+ ];
+
+ // Regions (gl/hl/ceid).
+ // Add more entries and they show up automatically.
+ window.NEWS_REGIONS = [
+ // ---- North America / default English ----
+ { id: "US_EN", label: "United States (English)", hl: "en-US", gl: "US", ceid: "US:en" },
+ { id: "US_ES", label: "United States (Spanish)", hl: "es-US", gl: "US", ceid: "US:es-419" },
+ { id: "CA_EN", label: "Canada (English)", hl: "en-CA", gl: "CA", ceid: "CA:en" },
+ { id: "CA_FR", label: "Canada (French)", hl: "fr-CA", gl: "CA", ceid: "CA:fr" },
+
+ // ---- UK / Ireland ----
+ { id: "GB_EN", label: "United Kingdom (English)", hl: "en-GB", gl: "GB", ceid: "GB:en" },
+ { id: "IE_EN", label: "Ireland (English)", hl: "en-IE", gl: "IE", ceid: "IE:en" },
+
+ // ---- Europe (supported Google News editions) ----
+ { id: "AT_DE", label: "Austria (Deutsch)", hl: "de-AT", gl: "AT", ceid: "AT:de" },
+
+ { id: "BE_FR", label: "Belgium (Français)", hl: "fr-BE", gl: "BE", ceid: "BE:fr" },
+ { id: "BE_NL", label: "Belgium (Nederlands)", hl: "nl-BE", gl: "BE", ceid: "BE:nl" },
+
+ { id: "BG_BG", label: "Bulgaria (Български)", hl: "bg-BG", gl: "BG", ceid: "BG:bg" },
+
+ { id: "CH_DE", label: "Switzerland (Deutsch)", hl: "de-CH", gl: "CH", ceid: "CH:de" },
+ { id: "CH_FR", label: "Switzerland (Français)", hl: "fr-CH", gl: "CH", ceid: "CH:fr" },
+
+ { id: "CZ_CS", label: "Czechia (Čeština)", hl: "cs-CZ", gl: "CZ", ceid: "CZ:cs" },
+
+ { id: "DE_DE", label: "Germany (Deutsch)", hl: "de-DE", gl: "DE", ceid: "DE:de" },
+
+ { id: "ES_ES", label: "Spain (Español)", hl: "es-ES", gl: "ES", ceid: "ES:es" },
+
+ { id: "FR_FR", label: "France (Français)", hl: "fr-FR", gl: "FR", ceid: "FR:fr" },
+
+ { id: "GR_EL", label: "Greece (Ελληνικά)", hl: "el-GR", gl: "GR", ceid: "GR:el" },
+
+ { id: "HU_HU", label: "Hungary (Magyar)", hl: "hu-HU", gl: "HU", ceid: "HU:hu" },
+
+ { id: "IT_IT", label: "Italy (Italiano)", hl: "it-IT", gl: "IT", ceid: "IT:it" },
+
+ { id: "LT_LT", label: "Lithuania (Lietuvių)", hl: "lt-LT", gl: "LT", ceid: "LT:lt" },
+
+ { id: "LV_EN", label: "Latvia (English)", hl: "en-LV", gl: "LV", ceid: "LV:en" },
+ { id: "LV_LV", label: "Latvia (Latviešu)", hl: "lv-LV", gl: "LV", ceid: "LV:lv" },
+
+ { id: "NL_NL", label: "Netherlands (Nederlands)", hl: "nl-NL", gl: "NL", ceid: "NL:nl" },
+
+ { id: "NO_NO", label: "Norway (Norsk)", hl: "no-NO", gl: "NO", ceid: "NO:no" },
+
+ { id: "PL_PL", label: "Poland (Polski)", hl: "pl-PL", gl: "PL", ceid: "PL:pl" },
+
+ { id: "PT_PT", label: "Portugal (Português)", hl: "pt-PT", gl: "PT", ceid: "PT:pt-150" },
+
+ { id: "RO_RO", label: "Romania (Română)", hl: "ro-RO", gl: "RO", ceid: "RO:ro" },
+
+ { id: "RS_SR", label: "Serbia (Srpski)", hl: "sr-RS", gl: "RS", ceid: "RS:sr" },
+
+ { id: "SE_SV", label: "Sweden (Svenska)", hl: "sv-SE", gl: "SE", ceid: "SE:sv" },
+
+ { id: "SI_SL", label: "Slovenia (Slovenščina)", hl: "sl-SI", gl: "SI", ceid: "SI:sl" },
+
+ { id: "SK_SK", label: "Slovakia (Slovenčina)", hl: "sk-SK", gl: "SK", ceid: "SK:sk" },
+
+ { id: "UA_UK", label: "Ukraine (Українська)", hl: "uk-UA", gl: "UA", ceid: "UA:uk" },
+ { id: "UA_RU", label: "Ukraine (Русский)", hl: "ru-UA", gl: "UA", ceid: "UA:ru" },
+
+ { id: "TR_TR", label: "Turkey (Türkçe)", hl: "tr-TR", gl: "TR", ceid: "TR:tr" },
+
+ // ---- Russia ----
+ { id: "RU_RU", label: "Russia (Русский)", hl: "ru-RU", gl: "RU", ceid: "RU:ru" },
+
+ // ---- Asia ----
+ { id: "JP_JA", label: "Japan (日本語)", hl: "ja-JP", gl: "JP", ceid: "JP:ja" },
+ { id: "KR_KO", label: "Korea (한국어)", hl: "ko-KR", gl: "KR", ceid: "KR:ko" },
+ { id: "CN_ZH", label: "China (简体中文)", hl: "zh-CN", gl: "CN", ceid: "CN:zh-Hans" },
+
+ // ---- India ----
+ { id: "IN_EN", label: "India (English)", hl: "en-IN", gl: "IN", ceid: "IN:en" },
+ { id: "IN_HI", label: "India (हिन्दी)", hl: "hi-IN", gl: "IN", ceid: "IN:hi" },
+ ];
+
+
+ window.NEWS_DEFAULT_FEED_ID = "top";
+ window.NEWS_DEFAULT_REGION_ID = "US";
+ })();
+
\ No newline at end of file
diff --git a/website/news/index.html b/website/news/index.html
new file mode 100644
index 0000000..9f184fc
--- /dev/null
+++ b/website/news/index.html
@@ -0,0 +1,53 @@
+
+
+
+
+ SumiNews (68k-style)
+
+
+
+
+
+
+
+
+
+
+
SumiNews
+
+
+
+
+
+
+
+
+
+
+
+ Uses Google News RSS.
+
+
+
+
+
+
+
+
+
+
+ Basic HTML news for old/new browsers. Inspired by the 68k.news approach (topics + region editions).
+ Based on LunarProject.org.
+
+
+
+
+
+
+
diff --git a/website/news/style.css b/website/news/style.css
new file mode 100644
index 0000000..194a4c0
--- /dev/null
+++ b/website/news/style.css
@@ -0,0 +1,109 @@
+/*
+ Lunar-ish styling (similar vibe to your Launchpad/Weather)
+*/
+
+html, body {
+ font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ color: #222;
+ background-color: #ddd;
+ margin: 0;
+ padding: 0.5rem;
+ }
+
+ a, a:visited {
+ text-decoration: none;
+ color: #a22121;
+ }
+
+ .center {
+ margin-left: auto;
+ margin-right: auto;
+ width: 100%;
+ max-width: 520px;
+ text-align: center;
+ }
+
+ .grid {
+ border: 1px solid #000;
+ padding: 10px;
+ background-color: #fff;
+ text-align: left;
+ }
+
+ h2 {
+ text-align: center;
+ margin: 0.4rem 0 0.6rem 0;
+ }
+
+ .controls {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ }
+
+ .controls label {
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ select, button {
+ font-size: 14px;
+ padding: 4px 6px;
+ }
+
+ button {
+ cursor: pointer;
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid #000;
+ margin: 10px 0;
+ }
+
+ .fineprint {
+ margin-top: 8px;
+ font-size: 12px;
+ text-align: left;
+ color: #333;
+ }
+
+ .error {
+ border: 1px solid #a22121;
+ background: #fff3f3;
+ color: #7a1111;
+ padding: 8px;
+ margin: 8px 0;
+ font-size: 13px;
+ }
+
+ .item {
+ padding: 8px 0;
+ border-bottom: 1px dashed #bbb;
+ }
+
+ .item:last-child {
+ border-bottom: none;
+ }
+
+ .item .title {
+ font-weight: bold;
+ display: inline-block;
+ }
+
+ .item .meta {
+ font-size: 12px;
+ color: #444;
+ margin-top: 2px;
+ }
+
+ .item .desc {
+ font-size: 13px;
+ margin-top: 6px;
+ color: #222;
+ }
+
\ No newline at end of file