Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
23 changes: 21 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ calendar: setup-python
@$(PYTHON) generate_calendar.py
@echo "Calendar generated successfully!"

# Optional responsive copies; originals remain the source of truth.
event-images:
@command -v convert >/dev/null && command -v identify >/dev/null || \
{ echo "Install ImageMagick to generate responsive event images."; exit 1; }
@mkdir -p images/events/responsive
@set -e; for image in images/events/*.jpg; do \
name=$$(basename "$$image" .jpg); \
width=$$(identify -ping -format '%w' "$$image"); \
for size in 480 960 1440; do \
target="images/events/responsive/$$name-$$size.webp"; \
if [ "$$width" -ge "$$size" ]; then \
if [ ! -f "$$target" ] || [ "$$image" -nt "$$target" ]; then \
convert "$$image" -auto-orient -resize "$${size}x" -strip -quality 80 "$$target"; \
fi; \
else \
rm -f "$$target"; \
fi; \
done; \
done

# Setup Python environment for calendar generation
setup-python: $(VENV)/.requirements-installed

Expand All @@ -76,5 +96,4 @@ $(VENV)/.requirements-installed: requirements.txt
@touch $@
@echo "Python environment ready!"

.PHONY: all build lint test check serve serve-livereload serve-windows calendar setup-python run clean install

.PHONY: all build lint test check serve serve-livereload serve-windows calendar event-images setup-python run clean install
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ This repository includes common shortcuts in [`makefile`](makefile):
- `serve-windows`: Live reload with force polling
- `clean`: Remove build and cache artifacts
- `calendar`: Generate `calendar.ics` from event data
- `event-images`: Generate optional responsive WebP copies of event banners (requires ImageMagick)

Examples:

Expand Down Expand Up @@ -80,7 +81,8 @@ Keep filenames lowercase and use the `.jpg` extension. No additional image
field is needed in the event data.

Upcoming event cards, past event cards, and event detail pages all use the same
local banner. Separate `-550.jpg` thumbnails are not required. If an image is
local banner. Upcoming cards and detail pages prefer an optional `<id>-teaser.jpg`
when present. Separate `-550.jpg` thumbnails are not required. If an image is
missing, the site displays `images/cloudnativelinz-banner.png` as a fallback.

Add new banners directly to `images/events/`, not the generated `_site/`
Expand All @@ -92,6 +94,19 @@ automatically. When creating or updating an event, commit the corresponding
`images/events/<id>.jpg` if a new or updated banner is needed. Changes to event
data do not regenerate existing banners.

After adding or replacing a banner or teaser, run `make event-images` with
ImageMagick installed (on Ubuntu: `sudo apt-get install imagemagick`). Commit the
generated `images/events/responsive/<name>-480.webp`, `-960.webp`, and `-1440.webp`
alongside the original. Copies preserve the aspect ratio, strip metadata, and are
only generated at widths no larger than the original; unchanged files are skipped.
When deleting or renaming an original, remove its responsive copies too.

The shared `_includes/event-image.html` advertises only existing copies through
`picture`, `srcset`, and layout-specific `sizes`. Browsers select a suitable
download for the display width and pixel density. Originals still work when no
copies exist or WebP is unsupported, and failed image loads retain the common
fallback. Generation is optional and is not required for a normal Jekyll build.

## Event URL Redirects

Event pages use stable slugs to avoid broken links when titles change.
Expand Down Expand Up @@ -138,4 +153,3 @@ Contributions are welcome.
2. Make your changes
3. Build locally and verify
4. Open a pull request

24 changes: 24 additions & 0 deletions _includes/event-image.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{%- assign image_name = include.path | split: '/' | last | remove: '.jpg' -%}
{%- assign image_widths = '480,960,1440' | split: ',' -%}
{%- assign image_srcset = '' -%}
{%- for image_width in image_widths -%}
{%- assign variant_path = '/images/events/responsive/' | append: image_name | append: '-' | append: image_width | append: '.webp' -%}
{%- assign variant_file = site.static_files | where: 'path', variant_path | first -%}
{%- if variant_file -%}
{%- unless image_srcset == '' -%}
{%- assign image_srcset = image_srcset | append: ', ' -%}
{%- endunless -%}
{%- assign variant_url = variant_path | relative_url -%}
{%- assign image_srcset = image_srcset | append: variant_url | append: ' ' | append: image_width | append: 'w' -%}
{%- endif -%}
{%- endfor -%}
<picture>
{% unless image_srcset == '' %}
<source type="image/webp" srcset="{{ image_srcset }}" sizes="{{ include.sizes | default: '(min-width: 1200px) 1168px, (min-width: 641px) calc(100vw - 48px), calc(100vw - 32px)' }}">
{% endunless %}
<img src="{{ include.path | relative_url }}" alt="{{ include.alt | escape }}"
class="{{ include.class | default: '' }}"
loading="{{ include.loading | default: 'lazy' }}" decoding="async"
{% if include.priority %}fetchpriority="{{ include.priority }}"{% endif %}
onerror="this.onerror=null;this.parentElement.querySelectorAll('source').forEach(source => source.remove());this.src='{{ '/images/cloudnativelinz-banner.png' | relative_url }}'" />
</picture>
2 changes: 1 addition & 1 deletion _includes/meta.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>

{% if page.excerpt %}
<meta property="og:description" content="{{ page.excerpt| strip_html }}" />
Expand Down
7 changes: 1 addition & 6 deletions _includes/upcoming-events.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@
<article class="event-card" data-event-id="{{ event.id }}">
<div class="event-banner{% if teaser_file %} event-banner--teaser{% endif %}">
<a href="{{ "/events/meetup/" | append: event.slug | append: "/" | relative_url }}">
<img class="{{ event_image_class }}"
src="{{ event_image_path | relative_url }}"
alt="{{ event.title }}"
loading="lazy"
onerror="this.onerror=null;this.src='{{ '/images/cloudnativelinz-banner.png' | relative_url }}'"
/>
{% include event-image.html path=event_image_path alt=event.title class=event_image_class %}
</a>
<div class="event-overlay">
<div class="event-date">
Expand Down
76 changes: 57 additions & 19 deletions _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

<meta name="theme-color" content="#667eea">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="32x32" href="{{ site.baseurl }}/images/favicon-32x32.png">
{% include analytics_head.html %}
</head>
Expand All @@ -49,14 +48,14 @@
</a>

<!-- Mobile menu button -->
<button class="mobile-menu-btn" aria-label="Toggle navigation menu" aria-expanded="false">
<button type="button" class="mobile-menu-btn" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="main-navigation">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>

<!-- Navigation -->
<nav class="site-nav" aria-label="Main navigation">
<nav id="main-navigation" class="site-nav" aria-label="Main navigation">
<a href="{{ site.baseurl }}/" class="nav-link">
<span class="nav-text">Home</span>
</a>
Expand Down Expand Up @@ -110,34 +109,73 @@
document.addEventListener('DOMContentLoaded', function() {
const mobileMenuBtn = document.querySelector('.mobile-menu-btn');
const siteNav = document.querySelector('.site-nav');
const body = document.body;
const header = document.querySelector('.site-header');
const mobileViewport = window.matchMedia('(max-width: 1024px)');

if (mobileMenuBtn && siteNav) {
if (mobileMenuBtn && siteNav && header) {
function setMenuOpen(open, restoreFocus = false) {
mobileMenuBtn.setAttribute('aria-expanded', String(open));
mobileMenuBtn.classList.toggle('active', open);
siteNav.classList.toggle('active', open);
if (restoreFocus) mobileMenuBtn.focus();
}

function updateMenuHeight() {
// Allow for the gradient bar above the sticky header before scrolling.
header.style.setProperty('--header-height', `${header.offsetHeight + 4}px`);
}

updateMenuHeight();
window.addEventListener('resize', updateMenuHeight);
if ('ResizeObserver' in window) {
new ResizeObserver(updateMenuHeight).observe(header);
}

mobileMenuBtn.addEventListener('click', function() {
const isExpanded = mobileMenuBtn.getAttribute('aria-expanded') === 'true';
mobileMenuBtn.setAttribute('aria-expanded', !isExpanded);
mobileMenuBtn.classList.toggle('active');
siteNav.classList.toggle('active');
body.classList.toggle('nav-open');
setMenuOpen(mobileMenuBtn.getAttribute('aria-expanded') !== 'true');
});

mobileMenuBtn.addEventListener('keydown', function(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setMenuOpen(true);
siteNav.querySelector('a').focus();
}
});

// Close menu when clicking on a link
siteNav.addEventListener('click', function(e) {
if (e.target.classList.contains('nav-link')) {
mobileMenuBtn.classList.remove('active');
siteNav.classList.remove('active');
body.classList.remove('nav-open');
mobileMenuBtn.setAttribute('aria-expanded', 'false');
if (mobileViewport.matches && e.target.closest('.nav-link')) {
setMenuOpen(false, true);
}
});

// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!siteNav.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenuBtn.classList.remove('active');
siteNav.classList.remove('active');
body.classList.remove('nav-open');
mobileMenuBtn.setAttribute('aria-expanded', 'false');
setMenuOpen(false, mobileViewport.matches && siteNav.contains(document.activeElement));
}
});

document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && mobileMenuBtn.getAttribute('aria-expanded') === 'true') {
e.preventDefault();
setMenuOpen(false, true);
}
});

header.addEventListener('focusout', function(e) {
if (e.relatedTarget && !header.contains(e.relatedTarget)) {
setMenuOpen(false);
}
});

mobileViewport.addEventListener('change', function() {
const focusInNav = siteNav.contains(document.activeElement);
const focusOnButton = document.activeElement === mobileMenuBtn;
setMenuOpen(false, mobileViewport.matches && focusInNav);
if (!mobileViewport.matches && focusOnButton) {
siteNav.querySelector('a').focus();
}
});
}
Expand Down
4 changes: 1 addition & 3 deletions _layouts/event.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
</nav>

<div class="event-artwork">
<img src="{{ artwork_path | relative_url }}" alt="{{ page.title | escape }}"
fetchpriority="high"
onerror="this.onerror=null;this.src='{{ '/images/cloudnativelinz-banner.png' | relative_url }}'" />
{% include event-image.html path=artwork_path alt=page.title loading="eager" priority="high" %}
</div>

<div class="event-page-content">
Expand Down
7 changes: 2 additions & 5 deletions _pages/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ permalink: events/
<article class="event-card" data-event-id="{{ event.id }}">
<div class="event-banner">
<a href="{{ site.baseurl }}/events/meetup/{{ event.slug }}/">
<img src="{{ '/images/events/' | append: event.id | append: '.jpg' | relative_url }}"
alt="{{ event.title }}"
loading="lazy"
onerror="this.onerror=null;this.src='{{ '/images/cloudnativelinz-banner.png' | relative_url }}'"
/>
{% assign banner_path = '/images/events/' | append: event.id | append: '.jpg' %}
{% include event-image.html path=banner_path alt=event.title sizes="(min-width: 1146px) 368px, (min-width: 1025px) calc(50vw - 32px), (min-width: 780px) calc(50vw - 40px), (min-width: 641px) calc(100vw - 48px), calc(100vw - 32px)" %}
</a>
<div class="event-overlay">
<div class="event-date">
Expand Down
Loading
Loading