From 88f891fd4e5e3c64524b9e45bbcb1b485174d5fb Mon Sep 17 00:00:00 2001 From: Nubenetes Bot Date: Fri, 19 Jun 2026 11:11:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=205=20improvements=20=E2=80=94=20geo=20in?= =?UTF-8?q?ference,=20last-updated=20badge,=20search=20boost,=20RSS=20feed?= =?UTF-8?q?,=20weekly=20cron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. geo_region inference: _infer_geo_from_url() infers Americas/Europe/España/Asia-Pacific from URL TLD (.es .de .fr .uk .jp .cn etc.) as fallback when geo_region field is empty. Industry digest now shows real content instead of empty categories. 2. Last-updated badge: trending section header now shows "Updated Jun 19, 2026" pill derived from news_digest.json mtime — gives readers confidence in freshness. 3. Search boost: tech-digest and industry-digest pages now have search.boost: 2 frontmatter so they rank higher in MkDocs Material site search. 4. RSS feed (src/rss_generator.py): generates v2-docs/feed.xml with top-20 curated picks from 3-month digest. Runs after news_digest in publisher. Autodiscovery added to v2-mkdocs.yml extra_head. 5. Weekly cron (09.weekly_digest.yml): runs every Monday 06:00 UTC, generates digest with Gemini, renders digest pages, commits, then triggers publisher. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/04.1.agentic_v2_publish.yml | 1 + .github/workflows/09.weekly_digest.yml | 70 +++++++++++ docs/static/v2_elite.css | 12 ++ src/news_digest.py | 29 ++++- src/rss_generator.py | 111 ++++++++++++++++++ src/v2_optimizer.py | 18 ++- v2-mkdocs.yml | 5 +- 7 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/09.weekly_digest.yml create mode 100644 src/rss_generator.py diff --git a/.github/workflows/04.1.agentic_v2_publish.yml b/.github/workflows/04.1.agentic_v2_publish.yml index 5f658ed0..f505ea98 100644 --- a/.github/workflows/04.1.agentic_v2_publish.yml +++ b/.github/workflows/04.1.agentic_v2_publish.yml @@ -93,6 +93,7 @@ jobs: GEMINI_API_KEY_2: ${{ secrets.GEMINI_API_KEY_2 }} run: | python -u -m src.news_digest || echo "News digest generation skipped (no API key or error)" + python -u -m src.rss_generator || echo "RSS generation skipped" - name: Run V2 Publisher (Render-Only) env: diff --git a/.github/workflows/09.weekly_digest.yml b/.github/workflows/09.weekly_digest.yml new file mode 100644 index 00000000..6931c41d --- /dev/null +++ b/.github/workflows/09.weekly_digest.yml @@ -0,0 +1,70 @@ +name: 09. Weekly Intelligence Digest + +on: + schedule: + # Every Monday at 06:00 UTC (08:00 Madrid time) + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: develop-git-write-lock + cancel-in-progress: false + +jobs: + weekly-digest: + runs-on: ubuntu-latest + steps: + - name: Repository Synchronization + uses: actions/checkout@v6 + with: + ref: develop + fetch-depth: 0 + + - name: Python 3.11 Environment Provisioning + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install Dependencies + run: pip install -r requirements.txt + + - name: Generate News Digest (Gemini) + env: + PYTHONPATH: ${{ github.workspace }} + GEMINI_API_KEY_1: ${{ secrets.GEMINI_API_KEY_1 }} + GEMINI_API_KEY_2: ${{ secrets.GEMINI_API_KEY_2 }} + run: | + python -u -m src.news_digest + python -u -m src.rss_generator || echo "RSS generation skipped" + + - name: Render V2 Portal (Digest Pages Only) + env: + PYTHONPATH: ${{ github.workspace }} + PYTHONUNBUFFERED: "1" + run: | + python -u -m src.v2_optimizer --render-only + + - name: Commit and Push Weekly Digest + run: | + git config --global user.name "Nubenetes Bot" + git config --global user.email "bot@nubenetes.com" + git add data/news_digest.json v2-docs/tech-digest.md v2-docs/industry-digest.md v2-docs/feed.xml || true + if git diff --staged --quiet; then + echo "No digest changes to commit." + else + git commit -m "feat: weekly intelligence digest update [skip ci]" + for i in {1..3}; do + git pull origin develop --rebase && git push origin develop && break || sleep 10 + done + fi + + - name: Trigger V2 Publisher + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + event-type: weekly-digest-ready diff --git a/docs/static/v2_elite.css b/docs/static/v2_elite.css index 16321c5d..e2155020 100644 --- a/docs/static/v2_elite.css +++ b/docs/static/v2_elite.css @@ -1043,6 +1043,18 @@ input[type="text"] { display: flex; align-items: center; gap: 8px; + flex-wrap: wrap; +} + +.trending-section__updated { + font-size: 0.55em; + font-weight: 500; + padding: 2px 10px; + border-radius: 20px; + background: rgba(14, 165, 233, 0.12); + color: var(--md-accent-fg-color); + border: 1px solid rgba(14, 165, 233, 0.25); + letter-spacing: 0.02em; } .trending-grid { diff --git a/src/news_digest.py b/src/news_digest.py index c45d78e8..a6ec756f 100644 --- a/src/news_digest.py +++ b/src/news_digest.py @@ -182,11 +182,38 @@ class NewsDigestEngine: return None def _get_entry_geo(self, entry: dict) -> str | None: - """Return the geo digest category if ``geo_region`` matches.""" + """Return the geo digest category using geo_region field, falling back to URL TLD inference.""" region = entry.get("geo_region", "") for geo_name, geo_val in self.GEO_CATEGORIES.items(): if region == geo_val: return geo_name + # Fallback: infer from URL TLD + return self._infer_geo_from_url(entry.get("url", "")) + + @staticmethod + def _infer_geo_from_url(url: str) -> str | None: + """Infer geo category from URL TLD. Returns GEO_CATEGORIES key or None.""" + try: + from urllib.parse import urlparse + host = urlparse(url).hostname or "" + # Ordered longest-first to avoid .uk matching before .co.uk + tld_to_region = [ + (".com.au", "Asia-Pacific"), (".co.uk", "Europe"), (".co.jp", "Asia-Pacific"), + (".co.kr", "Asia-Pacific"), (".com.br", "Americas"), (".com.mx", "Americas"), + (".es", "España"), (".de", "Europe"), (".fr", "Europe"), (".it", "Europe"), + (".pt", "Europe"), (".nl", "Europe"), (".be", "Europe"), (".se", "Europe"), + (".dk", "Europe"), (".fi", "Europe"), (".no", "Europe"), (".ch", "Europe"), + (".at", "Europe"), (".pl", "Europe"), (".cz", "Europe"), (".uk", "Europe"), + (".ie", "Europe"), (".eu", "Europe"), (".cn", "Asia-Pacific"), + (".jp", "Asia-Pacific"), (".kr", "Asia-Pacific"), (".sg", "Asia-Pacific"), + (".in", "Asia-Pacific"), (".au", "Asia-Pacific"), (".nz", "Asia-Pacific"), + (".ca", "Americas"), (".mx", "Americas"), (".br", "Americas"), + ] + for tld, region in tld_to_region: + if host.endswith(tld): + return region + except Exception: + pass return None @staticmethod diff --git a/src/rss_generator.py b/src/rss_generator.py new file mode 100644 index 00000000..6482c68e --- /dev/null +++ b/src/rss_generator.py @@ -0,0 +1,111 @@ +"""RSS 2.0 feed generator for the Nubenetes Intelligence Digest. + +Reads data/news_digest.json and writes v2-docs/feed.xml with the top +items from the 3-month digest window across all tech categories. +""" + +from __future__ import annotations + +import json +import os +from datetime import datetime +from email.utils import format_datetime +from xml.sax.saxutils import escape + +from src.logger import log_event + +DIGEST_PATH = "data/news_digest.json" +OUTPUT_PATH = "v2-docs/feed.xml" +FEED_TITLE = "Nubenetes Intelligence Digest" +FEED_LINK = "https://nubenetes.com/" +FEED_DESCRIPTION = "AI-curated top picks from the Cloud Native & Kubernetes ecosystem" +FEED_LANGUAGE = "en" +ITEMS_PER_FEED = 20 + +TECH_CATS = [ + "Kubernetes & Orchestration", "AI & Agents", "Security & Compliance", + "CI/CD & GitOps", "Observability, SRE & Testing", "Infrastructure as Code", + "Containers & Runtime", "Networking & Service Mesh", "Cloud Providers & FinOps", + "MLOps & Data Science", "Data, Messaging & Storage", +] + + +def _rfc822(dt: datetime) -> str: + return format_datetime(dt) + + +def generate_rss() -> None: + if not os.path.exists(DIGEST_PATH): + log_event("[WARN] rss_generator: news_digest.json not found, skipping RSS generation") + return + + try: + with open(DIGEST_PATH, "r", encoding="utf-8") as f: + digest = json.load(f) + except Exception as e: + log_event(f"[WARN] rss_generator: failed to load digest: {str(e)[:100]}") + return + + period_data = digest.get("3_months", {}) + items: list[dict] = [] + + for cat in TECH_CATS: + for entry in period_data.get(cat, []): + items.append({**entry, "_cat": cat}) + + # Sort by impact then date + impact_rank = {"critical": 3, "high": 2, "medium": 1} + items.sort( + key=lambda x: (impact_rank.get(x.get("impact", "medium"), 0), x.get("date", "")), + reverse=True, + ) + items = items[:ITEMS_PER_FEED] + + build_date = _rfc822(datetime.utcnow()) + + lines = [ + '', + '', + " ", + f" {escape(FEED_TITLE)}", + f" {FEED_LINK}", + f" {escape(FEED_DESCRIPTION)}", + f" {FEED_LANGUAGE}", + f" {build_date}", + f' ', + ] + + for item in items: + title = escape(item.get("title", "Unknown")) + url = item.get("url", "#") + why = escape(item.get("why", "")) + cat = escape(item.get("_cat", "")) + impact = item.get("impact", "medium") + date_str = item.get("date", "") + try: + pub_date = _rfc822(datetime.strptime(date_str, "%Y-%m-%d")) if date_str else build_date + except Exception: + pub_date = build_date + + lines += [ + " ", + f" {title}", + f" {url}", + f" {url}", + f" {pub_date}", + f" {cat}", + f" [{impact.upper()}] {why}", + " ", + ] + + lines += [" ", ""] + + os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) + with open(OUTPUT_PATH, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + + log_event(f"[INFO] rss_generator: wrote {len(items)} items to {OUTPUT_PATH}") + + +if __name__ == "__main__": + generate_rss() diff --git a/src/v2_optimizer.py b/src/v2_optimizer.py index 31b5fedc..fea9e908 100644 --- a/src/v2_optimizer.py +++ b/src/v2_optimizer.py @@ -953,8 +953,9 @@ class V2VisionEngine: geo_cats = ["Americas", "Europe", "España", "Asia-Pacific"] period_labels = {"3_months": "Last 3 Months", "6_months": "Last 6 Months", "12_months": "Last 12 Months"} - def render_digest_page(title, categories, digest_data): - md = f"# {title}\n\n" + def render_digest_page(title, categories, digest_data, search_boost=1): + md = f"---\nsearch:\n boost: {search_boost}\n---\n\n" + md += f"# {title}\n\n" md += "!!! tip \"Nubenetes Intelligence Digest\"\n" md += " AI-curated ranking of the most impactful resources, updated monthly.\n\n" for period_key, period_label in period_labels.items(): @@ -976,11 +977,11 @@ class V2VisionEngine: md += "\n" return md - tech_md = render_digest_page("📊 Nubenetes Tech & Cloud Intelligence Digest", tech_cats, digest_data) + tech_md = render_digest_page("📊 Nubenetes Tech & Cloud Intelligence Digest", tech_cats, digest_data, search_boost=2) with open(os.path.join(V2_DIR, "tech-digest.md"), "w", encoding="utf-8") as f: f.write(tech_md) - industry_md = render_digest_page("🌍 Nubenetes Industry & Geo Intelligence Digest", geo_cats, digest_data) + industry_md = render_digest_page("🌍 Nubenetes Industry & Geo Intelligence Digest", geo_cats, digest_data, search_boost=2) with open(os.path.join(V2_DIR, "industry-digest.md"), "w", encoding="utf-8") as f: f.write(industry_md) @@ -1006,7 +1007,14 @@ class V2VisionEngine: top_items = top_items[:6] impact_icons = {"critical": "🔴", "high": "🟡", "medium": "🔵"} - cards_html = '