Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions content/en/_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,17 @@ <h2>The Absolute Beginner's Guide To Cloud Native</h2>
{{< /blocks/section >}}
{{< blocks/section id="upcoming-events" >}}
<h2>Attend upcoming KubeCon + CloudNativeCon events</h2>
<!-- TODO: change this to a shortcode that auto-updates from a schedule -->
<div>
<a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe-2026/" class="desktopKCButton"><strong>Europe</strong> (Amsterdam, Mar 23-26, 2026)</a>
</div>
<div>
<a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-north-america-2026/" class="desktopKCButton"><strong>North America</strong> (Salt Lake City, Nov 9-12, 2026)</a>
</div>
<!--
{{< kubecon-events >}} renders event listings from data/events/kubecon.yaml.
Note for localization:
- Region names support i18n via kubecon_region_* keys.
- City names remain in English.
- Dates are stored in ISO 8601 and formatted by Hugo's time.Format.
- Localized pages may replace this shortcode with a static, fully localized version if needed.
- If a static version is used, keeping event information up to date becomes the responsibility of the localization team.
-->
{{< kubecon-events >}}
Comment thread
lmktfy marked this conversation as resolved.

{{< /blocks/section >}}

{{< blocks/kubernetes-features >}}
Expand Down
35 changes: 35 additions & 0 deletions data/events/kubecon.yaml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: I would use the path data/events/kubecon.yaml (and revise the other code accordingly).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thank you

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Do not manually edit this file
# This file is auto-generated by scripts/fetch_kubecon_events.py
# To update, run:
# python3 scripts/fetch_kubecon_events.py
#
# Last updated: 2026-03-27 15:54:46
# Source: https://events.linuxfoundation.org/about/calendar/?_sf_s=kubecon

events:
- name: KubeCon + CloudNativeCon India
start_date: '2026-06-18'
end_date: '2026-06-19'
location:
announced: true
city: Mumbai
country: India
url: https://events.linuxfoundation.org/kubecon-cloudnativecon-india/
colors:
- '#f5d4a1'
- '#eeb32d'
region: India
- name: KubeCon + CloudNativeCon Japan
start_date: '2026-07-29'
end_date: '2026-07-30'
location:
announced: true
city: Yokohama
country: Japan
url: https://events.linuxfoundation.org/kubecon-cloudnativecon-japan/
colors:
- '#ffffff'
- '#ffaebf'
region: Japan
last_updated: '2026-03-27 15:54:46'
source: https://events.linuxfoundation.org/about/calendar/?_sf_s=kubecon
18 changes: 18 additions & 0 deletions i18n/en/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,24 @@ of Katacoda.</p>
2023</b>. You are seeing this notice because this particular page has not yet been updated
following that shutdown.</p>"""

[kubecon_region_china]
other = "China"

[kubecon_region_europe]
other = "Europe"

[kubecon_region_event]
other = "Event"

[kubecon_region_india]
other = "India"

[kubecon_region_japan]
other = "Japan"

[kubecon_region_north_america]
other = "North America"

[kubectl_ref_collapse_all]
other = "Collapse all"

Expand Down
1 change: 0 additions & 1 deletion i18n/pl/pl.toml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was great as an example, but please now remove this and squash it out so we can move the pull request forward.

Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ other = "Wyszukaj polecenia lub opis ..."

[kubectl_ref_view_full]
other = "Zobacz pełną dokumentację"

Copy link
Copy Markdown
Member

@dkarczmarski dkarczmarski May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the change at line 290 seems unnecessary :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we should probably not change localizations in this PR.

[kubernetes]
other = "Kubernetes"

Expand Down
18 changes: 18 additions & 0 deletions layouts/shortcodes/kubecon-events.html
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has English text in it, and as such will be difficult to localize. I am not yet sure that merging this PR (as is) would be an improvement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the city names and dates are still English because from the conversations, I think it's okay to keep them in English across all locales (or what do you think? everything should be localize?). For the date I can switch to ISO the Hugo time formatter.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{ $data := .Site.Data.events.kubecon }}

{{ range $data.events -}}
{{ $startDate := time.AsTime .start_date -}}
{{ $endDate := time.AsTime .end_date -}}
{{ $regionKeys := dict "North America" "kubecon_region_north_america" "Europe" "kubecon_region_europe" "India" "kubecon_region_india" "Japan" "kubecon_region_japan" "China" "kubecon_region_china" "Event" "kubecon_region_event" }}
Copy link
Copy Markdown
Member

@lmktfy lmktfy May 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could put this mapping in (eg) data/events/region_name_heuristics.yaml

{{ $regionKey := index $regionKeys .region | default "kubecon_region_event" }}
{{ $regionName := T $regionKey }}
{{ $color := "" }}
{{ with .colors }}{{ $color = index . 0 }}{{ end }}
<div>
{{ if .location.announced -}}
<a href="{{ .url }}" class="desktopKCButton"><strong>{{ $regionName }}</strong> ({{ .location.city }}, {{ $startDate | time.Format "Jan 2" }}–{{ $endDate | time.Format "2" }})</a>
{{ else -}}
<a href="{{ .url }}" class="desktopKCButton"><strong>{{ $regionName }}</strong> (TBA)</a>
{{ end -}}
</div>
{{- end }}
270 changes: 270 additions & 0 deletions scripts/fetch_kubecon_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
Fetch KubeCon events from Linux Foundation website and generate Hugo data file.

Uses JSON-LD structured data (schema.org/Event) from individual event pages
for reliable date/location extraction, and CSS custom properties for event colors.

Requirements: pip install requests beautifulsoup4 pyyaml
"""

import json
import requests
from bs4 import BeautifulSoup
import yaml
import re
from datetime import datetime
from typing import Optional

# Configuration
CALENDAR_URL = "https://events.linuxfoundation.org/about/calendar/?_sf_s=kubecon"
EVENT_LIMIT = 2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why 2? If there are 3 events coming up this would skip one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if we really want to show 3 upcoming events, change this locally and run the script with that setting.

OUTPUT_PATH = "data/events/kubecon.yaml"

HEADERS = {
'User-Agent': 'kubernetes-website-bot/1.0 (+https://github.com/kubernetes/website)'
}

REGIONS = {
"North America": "North America",
"Europe": "Europe",
"India": "India",
"Japan": "Japan",
"China": "China",
}
Comment on lines +28 to +34
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be tidier.


KUBECON_TITLE_MATCH_RE = re.compile(
r'^KubeCon \+ CloudNativeCon (' + '|'.join(re.escape(r) for r in REGIONS) + r')\s*\d*$'
)

def _is_valid_kubecon_title(title: str) -> bool:
"""Check if title matches the expected KubeCon event pattern."""
if KUBECON_TITLE_MATCH_RE.match(title):
return True
print(f" Skipped (not a main KubeCon event): {title}")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit)

Suggested change
print(f" Skipped (not a main KubeCon event): {title}")
print(f" Skipped (not a main KubeCon event): {title}", file=sys.stderr)

return False


def _extract_region(title: str) -> str:
"""Extract region from event title."""
m = KUBECON_TITLE_MATCH_RE.match(title)
return REGIONS[m.group(1)] if m else "Event"


def _extract_json_ld_event(soup: BeautifulSoup) -> Optional[dict]:
"""Extract schema.org Event from JSON-LD script tags."""
for script in soup.find_all('script', type='application/ld+json'):
try:
data = json.loads(script.string)
except (json.JSONDecodeError, TypeError):
continue

# Direct Event object
if isinstance(data, dict) and data.get('@type') == 'Event':
return data

# Check inside @graph arrays
if isinstance(data, dict) and '@graph' in data:
for item in data['@graph']:
if isinstance(item, dict) and item.get('@type') == 'Event':
return item

# Array of objects
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and item.get('@type') == 'Event':
return item

return None


def _extract_event_colors(soup: BeautifulSoup) -> Optional[list[str]]:
"""Extract event brand colors from CSS custom properties."""
colors = []
for style in soup.find_all('style'):
if not style.string:
continue
for match in re.findall(r'--event-color-\d+:\s*(#[0-9a-fA-F]{3,8})', style.string):
if match not in colors:
colors.append(match)
return colors if colors else None


def _parse_address(addr) -> Optional[dict]:
"""Parse a JSON-LD address into a location dict."""
if isinstance(addr, dict):
city = addr.get('addressLocality', '')
country = addr.get('addressCountry', '')
if city or country:
result = {'announced': True}
if city:
result['city'] = city
if country:
result['country'] = country
return result
if isinstance(addr, str) and addr:
return {'announced': True, 'city': addr}
return None


def _build_location(ld_event: dict) -> dict:
"""Build structured location dict from JSON-LD location field."""
location = ld_event.get('location')
if not location:
return {'announced': False}

places = location if isinstance(location, list) else [location]

for place in places:
if not isinstance(place, dict):
continue
result = _parse_address(place.get('address', {}))
if result:
return result

return {'announced': False}


def _fetch_event_detail(url: str) -> Optional[dict]:
"""Fetch an individual event page and extract JSON-LD + colors."""
print(f" Fetching event page: {url}")
response = requests.get(url, headers=HEADERS)
response.raise_for_status()

soup = BeautifulSoup(response.text, 'html.parser')
ld_event = _extract_json_ld_event(soup)
if not ld_event:
print(f" Warning: No JSON-LD Event found at {url}")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit)

Suggested change
print(f" Warning: No JSON-LD Event found at {url}")
print(f" Warning: No JSON-LD Event found at {url}", file=sys.stderr)

return None

colors = _extract_event_colors(soup)

result = {
'name': ld_event.get('name', ''),
'start_date': ld_event.get('startDate', ''),
'end_date': ld_event.get('endDate', ''),
'location': _build_location(ld_event),
'url': url,
}

if colors:
result['colors'] = colors

return result


def _discover_event_urls(calendar_url: str = CALENDAR_URL) -> list[tuple[str, str]]:
"""Scrape calendar page for KubeCon event URLs. Returns list of (title, url)."""
print(f"Fetching calendar: {calendar_url}")
response = requests.get(calendar_url, headers=HEADERS)
response.raise_for_status()

soup = BeautifulSoup(response.text, 'html.parser')
event_articles = soup.find_all('article', class_='callout')
print(f"Found {len(event_articles)} total events on page\n")

results = []
for article in event_articles:
title_elem = article.find('h5')
if not title_elem:
continue
link_elem = title_elem.find('a')
if not link_elem:
continue

title = link_elem.get_text(strip=True)
url = link_elem.get('href', '')

if _is_valid_kubecon_title(title) and url:
results.append((title, url))
print(f" Matched: {title}")

return results


def fetch_kubecon_events(calendar_url: str = CALENDAR_URL) -> list[dict]:
"""Fetch KubeCon events using JSON-LD structured data from event pages."""
event_urls = _discover_event_urls(calendar_url)

events = []
for _, url in event_urls:
try:
event_data = _fetch_event_detail(url)
if event_data:
event_data['region'] = _extract_region(event_data['name'])
events.append(event_data)
print(f" -> {event_data['name']} ({event_data['start_date']} to {event_data['end_date']})\n")
except Exception as e:
print(f" Error fetching {url}: {e}")
continue

return events


def generate_yaml_file(events: list[dict], output_path: str = OUTPUT_PATH, limit: int = EVENT_LIMIT) -> None:
"""Generate Hugo data file."""
if limit and limit > 0:
events = events[:limit]
print(f"\nLimiting to first {limit} events")

data = {
'events': events,
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'source': CALENDAR_URL
}

with open(output_path, 'w') as f:
f.write("# Do not manually edit this file\n")
f.write("# This file is auto-generated by scripts/fetch_kubecon_events.py\n")
f.write("# To update, run:\n")
f.write("# python3 scripts/fetch_kubecon_events.py\n")
f.write("#\n")
f.write(f"# Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"# Source: {CALENDAR_URL}\n")
f.write("\n")
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)

print(f"\nGenerated {output_path}")
print(f" {len(events)} KubeCon events")


if __name__ == '__main__':
print("=" * 60)
print("KubeCon Events Fetcher (JSON-LD)")
print("=" * 60)

try:
events = fetch_kubecon_events()

if not events:
print("\nNo matching KubeCon events found!")
print(" Looking for: 'KubeCon + CloudNativeCon <region>'")
Comment on lines +240 to +241
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could write this to stderr

exit(1)

print(f"\n{'=' * 60}")
print("Generating YAML data file...")
print("=" * 60)

generate_yaml_file(events)

print("\n" + "=" * 60)
print("Success!")
print("=" * 60)
print("\nMatched events:")
for i, event in enumerate(events[:EVENT_LIMIT], 1):
print(f" {i}. {event['name']}")
loc = event['location']
loc_str = f"{loc.get('city', 'TBA')}, {loc.get('country', '')}" if loc.get('announced') else "TBA"
print(f" {loc_str} - {event['start_date']} to {event['end_date']}")
if 'colors' in event:
print(f" Colors: {', '.join(event['colors'])}")

except requests.exceptions.RequestException as e:
print(f"\nNetwork Error: {e}")
print(" Could not fetch the events page.")
Comment on lines +263 to +264
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could write this to stderr

exit(1)
except Exception as e:
print(f"\nError: {e}")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
print(f"\nError: {e}")
print(f"\nError: {e}", file=sys.stderr)

import traceback
traceback.print_exc()
exit(1)
2 changes: 2 additions & 0 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
requests>=2.28,<3
click>=8.0,<9
jinja2>=3.1,<4
beautifulsoup4>=4.12.0
pyyaml>=6.0