PaddelBuch.ch is a website which displays the data published in the public database of Swiss paddle sports information visually on a range of maps.
More information about the technological evolution of the project can be found in the Paddel Buch blog series on Cloudy Pandas.
- Contributing Guide — How to contribute to the project
- Architecture Overview — Build pipeline, data flow, and system design
- Plugin Reference — All 20 custom Jekyll plugins documented
- Frontend Guide — JavaScript modules, SCSS structure, and vendor assets
- Content Model — Contentful content types and how to add new ones
- Testing Guide — Test suites, property-based testing, and how to write new tests
- Deployment — AWS Amplify deployment and CloudFormation
- Custom Build Image — Docker build image for Amplify
Paddel Buch started following a sea kayaking roundtable meeting organised by Swiss Canoe in June 2021.
The main goal of this project is to provide a central, nation-wide store of information for all types of paddlers in Switzerland, to enable members of the paddle sports community to better plan their trips and explore new waterways.
- Static Site Generator: Jekyll 4.3
- CMS: Contentful (headless CMS) with Sync API for incremental updates
- Maps: Leaflet.js with OpenStreetMap tiles
- Hosting: AWS Amplify (eu-central-1)
- Languages: German (default), English
- Ruby: 3.4.9 (managed with chruby)
- Testing: RSpec + Rantly (Ruby), Jest + fast-check (JavaScript)
paddelbuch/
├── _config.yml # Jekyll configuration
├── _config_de.yml # German locale build override
├── _config_en.yml # English locale build override
├── _config_prefetch.yml # Contentful pre-fetch build override
├── _data/ # Data files (populated from Contentful)
│ ├── spots.yml # Spot data
│ ├── waterways.yml # Waterway data
│ ├── obstacles.yml # Obstacle data
│ ├── notices.yml # Event notice data
│ ├── protected_areas.yml
│ ├── static_pages.yml # CMS-driven static pages
│ └── types/ # Dimension/lookup tables
├── _i18n/ # Internationalization files (de.yml, en.yml)
├── _includes/ # Reusable HTML partials
│ ├── header.html # Site navigation
│ ├── footer.html # Site footer
│ ├── map-init.html # Leaflet map initialization
│ ├── detail-map-layers.html # Data layers for detail pages
│ ├── layer-control.html # Map layer toggle control
│ ├── spot-popup.html # Spot marker popup
│ ├── obstacle-popup.html
│ ├── event-popup.html
│ ├── rejected-popup.html # Rejected spot popup
│ └── *-detail-content.html # Detail page content partials
├── _layouts/ # Page templates
│ ├── default.html # Base layout
│ ├── page.html # Static page layout (CMS content)
│ ├── spot.html # Spot detail pages
│ ├── waterway.html # Waterway detail pages
│ ├── obstacle.html # Obstacle detail pages
│ └── notice.html # Event notice detail pages
├── _plugins/ # Jekyll plugins
│ ├── api_generator.rb # JSON API generation
│ ├── batch_fetcher.rb # Batched entry fetching for delta sync
│ ├── cache_metadata.rb # Sync state persistence
│ ├── collection_generator.rb # Collection page generation
│ ├── contentful_fetcher.rb # Contentful data fetching
│ ├── contentful_mappers.rb # Contentful → Jekyll data mapping
│ ├── dashboard_metrics_generator.rb # Data quality dashboard metrics
│ ├── env_loader.rb # .env file loading
│ ├── favicon_generator.rb # Favicon and Apple Touch Icon handling
│ ├── i18n_patch.rb # i18n compatibility patch
│ ├── locale_filter.rb # Locale-aware filtering
│ ├── ssl_patch.rb # SSL fix for Ruby 3.4+/OpenSSL 3.x
│ ├── statistics_metrics_generator.rb # Statistics dashboard metrics
│ ├── sync_checker.rb # Contentful Sync API integration
│ ├── tile_generator.rb # Spatial tile generation
│ └── waterway_filters.rb # Waterway-specific filters
├── _sass/ # SCSS stylesheets
├── _spots/ # Spot collection (generated)
├── _waterways/ # Waterway collection (generated)
├── _obstacles/ # Obstacle collection (generated)
├── _notices/ # Event notice collection (generated)
├── _static_pages/ # Static page collection (generated)
├── _tests/ # JavaScript test files
│ ├── unit/ # Unit tests (Jest)
│ └── property/ # Property-based tests (fast-check)
├── spec/ # Ruby test files (RSpec + Rantly)
│ ├── *_spec.rb # Unit and property-based tests
│ └── spec_helper.rb # Test configuration
├── api/ # Generated JSON API files
├── assets/ # Static assets
│ ├── css/ # Compiled CSS
│ ├── images/ # Images and icons
│ └── js/ # JavaScript modules
├── deploy/ # Deployment configuration
├── docs/ # Project documentation
├── gewaesser/ # Waterway list pages
├── offene-daten/ # Open data/API pages
├── scripts/ # Utility and build helper scripts
│ ├── clip_geometry_to_switzerland.py # Clip GeoJSON geometry to Swiss border
│ ├── clip_waterways_to_switzerland.py # Clip waterway geometries to Swiss border
│ ├── copy-vendor-assets.js # Copies vendor JS/CSS from node_modules
│ ├── cut_rivers_at_lakes.py # Cut river geometries at lake boundaries
│ ├── download-google-fonts.js # Downloads and self-hosts Google Fonts
│ ├── recalculate_river_lengths.py # Recalculate river lengths from geometry
│ ├── generate_apple_touch_icon.py # SVG → PNG icon generation
│ ├── restore_geometry_from_full.rb # Restores waterway geometry from full GeoJSON
│ └── simplify_waterway_geometry.rb # Simplifies waterway geometry in Contentful
├── amplify.yml # AWS Amplify build configuration
├── Gemfile # Ruby dependencies
└── package.json # Node.js dependencies (for testing)
- Ruby 3.4.9 (managed with chruby)
- Bundler
- Node.js (for running tests)
- librsvg (
brew install librsvg) — for regenerating the Apple Touch Icon PNG
Environment variables are loaded automatically from .env files by the _plugins/env_loader.rb plugin. The file loaded depends on JEKYLL_ENV:
JEKYLL_ENV |
File loaded | Default? |
|---|---|---|
development |
.env.development |
Yes |
production |
.env.production |
No |
Copy .env.example to .env.development and fill in your values:
cp .env.example .env.developmentRequired variables:
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_access_token
CONTENTFUL_ENVIRONMENT=master
MAPBOX_URL=your_mapbox_tile_url
SITE_URL=http://localhost:4000System environment variables always take priority over .env file values.
# Install Ruby dependencies
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && bundle install
# Install Node.js dependencies (for testing)
npm install# Start Jekyll development server (loads .env.development)
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && bundle exec jekyll serveThe site will be available at http://localhost:4000
# Test a production build locally (loads .env.production)
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && JEKYLL_ENV=production bundle exec rake build:siteThe build:site Rake task runs a parallel build pipeline that reduces build time by ~40% compared to the previous sequential approach:
- Pre-fetch — A single Jekyll invocation triggers
ContentfulFetcherto populate_data/with fresh Contentful content. - Parallel builds — Two Jekyll processes run concurrently, one per locale (
deanden), each writing to its own temporary output directory. - Merge — The German build output (root pages,
assets/,api/) and the English build output (en/subtree only) are combined into_site/.
The built site will be in the _site/ directory.
# Run all JavaScript tests (Jest + fast-check)
npm test
# Run property-based tests only
npm run test:property
# Run tests in watch mode
npm run test:watch
# Run Ruby tests (RSpec + Rantly)
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && bundle exec rspecScan Ruby and Node.js dependencies for known vulnerabilities:
# Scan Ruby gems only
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && bundle exec rake audit
# Scan both Ruby gems and npm packages
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && bundle exec rake audit:allThe site uses an SVG favicon (assets/images/logo-favicon.svg) for modern browsers and a 180×180 PNG Apple Touch Icon (assets/images/apple-touch-icon.png) for iOS devices.
The favicon_generator.rb plugin handles both during the Jekyll build:
- Copies the SVG to
/favicon.icoat the site root (prevents browser 404s) - Copies the PNG to
/apple-touch-icon.pngat the site root (where iOS looks for it)
The PNG is checked into the repo so production builds on AWS Amplify don't need any image conversion tools. If you update the SVG favicon, regenerate the PNG locally:
# Requires: brew install librsvg
python3 scripts/generate_apple_touch_icon.pyThe script uses a SHA-256 checksum to skip regeneration when the SVG hasn't changed.
Content is managed in Contentful and synced to Jekyll data files during the build process. The sync pipeline consists of several custom plugins:
- ContentfulFetcher (
contentful_fetcher.rb) — Orchestrates the data fetch from Contentful. Uses the Sync API to detect changes, then either performs a targeted delta merge (upserting changed entries and removing deleted entries in the existing YAML files) or falls back to a full re-fetch. Delta merge uses batchedclient.entries()calls withsys.id[in]filtering to fetch changed entries grouped by content type, reducing HTTP requests from O(N) per-entry to O(C) per-content-type. - SyncChecker (
sync_checker.rb) — Queries the Contentful Sync API and classifies delta items into changed entries, deleted entries, and unknown content types - BatchFetcher (
batch_fetcher.rb) — Fetches changed entries in batches during delta sync, grouping IDs by content type and handling sub-batching, pagination, and fallback to individual fetches on failure - CacheMetadata (
cache_metadata.rb) — Persists sync state (tokens, timestamps, content hash, and the Entry ID Index) between builds to enable incremental syncing and delta merges - ContentfulMappers (
contentful_mappers.rb) — Transforms Contentful entries into Jekyll-compatible YAML data, including rich text rendering with support for tables, marks (bold, italic, underline, code), and embedded entries - CollectionGenerator (
collection_generator.rb) — Generates Jekyll collection pages from the synced data
Content types mapped from Contentful include spots, waterways, obstacles, protected areas, event notices, static pages, and various dimension/lookup types.
By default, the build uses the Contentful Sync API for incremental updates. To force a full re-fetch of all content, you can either:
- Set the
CONTENTFUL_FORCE_SYNCenvironment variable:
source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.9 && CONTENTFUL_FORCE_SYNC=true bundle exec jekyll build- Or add
force_contentful_sync: trueto_config.yml:
force_contentful_sync: trueA full sync is also triggered automatically when no cache metadata exists, the cache is invalid, or the Contentful space/environment has changed since the last build.
The site is deployed to AWS Amplify using the CloudFormation template at deploy/frontend-deploy.yaml. The build configuration is defined in amplify.yml.
Once deployed, builds are triggered automatically when:
- Code is pushed to the configured branch
- Content is published in Contentful (via webhook)
| Parameter | Description | Required |
|---|---|---|
AppName |
Name for the Amplify app | Yes |
AppDescription |
Description for the Amplify app | Yes |
AppStage |
Deployment stage (PRODUCTION, BETA, DEVELOPMENT, EXPERIMENTAL, PULL_REQUEST) |
Yes |
AppDomainName |
Custom domain name (e.g. paddelbuch.ch) |
Production only |
GithubRepoUrl |
GitHub repository URL | Yes |
GithubBranchName |
Branch to deploy | Yes |
GithubToken |
GitHub personal access token | Yes |
EnvVarMapboxUrl |
MapBox tile style URL | Yes |
EnvVarContentfulToken |
Contentful API access token | Yes |
EnvVarContentfulSpace |
Contentful space ID | Yes |
EnvVarContentfulEnv |
Contentful environment ID | Yes |
EnvVarSiteUrl |
Site URL used during Jekyll build | Production only |
Production deployments configure a custom domain with both root and www subdomains, and set up a redirect from the naked domain to www.
noglob aws cloudformation deploy \
--template-file deploy/frontend-deploy.yaml \
--stack-name paddelbuch-prod \
--region eu-central-1 \
--parameter-overrides \
AppName=paddelbuch \
AppDescription="Paddel Buch production" \
AppStage=PRODUCTION \
AppDomainName=paddelbuch.ch \
GithubRepoUrl=https://github.com/your-org/paddelbuch \
GithubBranchName=main \
GithubToken=ghp_xxxxxxxxxxxx \
EnvVarMapboxUrl=your_mapbox_url \
EnvVarContentfulToken=your_token \
EnvVarContentfulSpace=your_space_id \
EnvVarContentfulEnv=master \
EnvVarSiteUrl=https://www.paddelbuch.chNon-production deployments (any AppStage other than PRODUCTION) skip custom domain configuration entirely. The site is accessible only via the default Amplify-provided URL (e.g. https://branch.xxxxxxxxxxxx.amplifyapp.com).
AppDomainName and EnvVarSiteUrl can be omitted for non-production stacks. The SITE_URL environment variable is automatically set to the default Amplify URL.
noglob aws cloudformation deploy \
--template-file deploy/frontend-deploy.yaml \
--stack-name paddelbuch-dev \
--region eu-central-1 \
--parameter-overrides \
AppName=paddelbuch-dev \
AppDescription="Paddel Buch development" \
AppStage=DEVELOPMENT \
GithubRepoUrl=https://github.com/your-org/paddelbuch \
GithubBranchName=develop \
GithubToken=ghp_xxxxxxxxxxxx \
EnvVarMapboxUrl=your_mapbox_url \
EnvVarContentfulToken=your_token \
EnvVarContentfulSpace=your_space_id \
EnvVarContentfulEnv=developmentPaddel Buch provides a JSON API for accessing paddle sports data. Documentation is available at /offene-daten/api.
Fact Tables:
/api/spots-{locale}.json- All spots/api/obstacles-{locale}.json- All obstacles/api/notices-{locale}.json- Event notices/api/protected-areas-{locale}.json- Protected areas/api/waterways-{locale}.json- Waterways
Dimension Tables:
/api/spottypes-{locale}.json/api/obstacletypes-{locale}.json/api/paddlecrafttypes-{locale}.json- And more...
Metadata:
/api/lastUpdateIndex.json- Last update timestamps
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Full license details can be found here: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
