# Hackertours Web Site ## Complete the checkout We need the submodule for the theme, so update/clone the submodules: ```shell git submodule update --init --recursive ``` ## Run in dev mode ```shell hugo server --buildFuture --buildDrafts ``` Then open http://localhost:1313 ## Shortcode: Language badge Use the `lang` shortcode to mark whether a tour is in German, English or bilingual. It renders a small superscript badge next to the text, with details when hovered over or parsed by a screenreader. - German: `{{< lang de >}}` - English: `{{< lang en >}}` - Bilingual: `{{< lang deen >}}` ## Shortcode: Tour schedule The tour overview of an event (`content/*//_index.md`) shows the schedule with `{{< schedule >}}`. It reads `data/schedule/.yaml`, which is shared by both languages: ```yaml days: - day: 1 date: "2025-12-27" tours: - { time: "11:30", tour: xfel, lang: deen } - { time: "12:00", tour: city-walk, lang: de } ``` - `tour` is the slug of the tour page in the event section. The card shows that page's title, without the event prefix, and links to it. - `lang` is `de`, `en` or `deen`, or a list of these. It renders the same badge as the `lang` shortcode. - `time` is `HH:MM` with a leading zero (`09:30`). Tours are sorted by time, so their order in the file doesn't matter. - `title` (optional) replaces the page's title on the card, with one entry per language: `title: { de: "Hafen am Abend", en: "Harbour by Night" }`. - `status` (optional): `soldout` marks the tour as fully booked. Other values are ignored. For the events managed with Hackertours-Admin, the data file is generated by its website sync; don't edit it by hand. Wide screens get one column per day on an hour grid, with tours that start at the same time side by side. The first column shows the hour wherever the first day has no tour in it. On narrow screens a day picker shows one day at a time, starting with today's day during the event. For a different event than the page's section, pass its name: `{{< schedule 39c3 >}}`. Without a data file, or without any tours in it, the shortcode renders nothing and the build logs a warning. To silence it, for example for an upcoming event, add `warning-schedule-empty` to `ignoreLogs` in `hugo.yaml`. ## Shortcode: Calendar entry `{{< calendar >}}` adds a small pill after a tour date with a calendar icon and two links: "iCal download", a calendar file (`.ics`) for that date, and "Google calendar", which adds the date in Google Calendar. Hackertours-Admin puts it into the list of dates on each tour page: ```markdown * Tag 1 (27.12.) **17:30 Uhr** {{< calendar date="2026-12-27" time="17:30" duration="210" >}} ``` | Param | Required | Content | |---|---|---| | `date` | yes | `YYYY-MM-DD` | | `time` | yes | Start time `HH:MM`, local time in Hamburg | | `duration` | no | Length in minutes. Without it, `duration` of the tour in `tours` in the schedule file is used (see "Map data in the schedule file"); without either, the calendar entry has no end time and the build logs a warning with the ID `warning-calendar-duration`. | | `title` | no | Title of the calendar entry. Without it, the `title` of this date in the schedule file is used, then the page title. | The `.ics` file is in the iCalendar format (RFC 5545), which Apple Calendar, Outlook, Thunderbird and most other calendar apps open directly. The Google calendar link is there because the Google Calendar app on Android can't open downloaded `.ics` files. The icon is `regular/calendar-plus` from [Font Awesome Free](https://fontawesome.com/search?ic=free), kept in [assets/vendor/fontawesome/](assets/vendor/fontawesome/). Other templates can use any of its icons with `{{ partial "fa-icon.html" "solid/ship" }}`: it inlines the SVG, decorative and in the text colour. The icons are CC BY 4.0; the attribution is the comment inside each SVG, which the partial keeps. Each entry starts at the time given, in Hamburg time, at the Hackertours desk (with `map.origin` from the schedule file as the address, if there is one). Its description is the tour summary and a link to the tour page. The files are generated next to the tour page, one per date and language, e.g. `/de/40c3/tour-of-the-giants/tour-of-the-giants-2026-12-27-1730.ics`. ## Shortcode: Tour map `{{< tour-map >}}` on an event's overview page shows a map of the tour locations, with the routes from the Congress, on OpenStreetMap. For a different event than the page's section, pass its name: `{{< tour-map 39c3 >}}`. It uses [Leaflet](https://leafletjs.com), which is kept in [assets/vendor/leaflet/](assets/vendor/leaflet/). - Each tour gets a marker in its colour, with its `icon` in white, and its route as a line; walking legs are dotted. Clicking a marker or a route shows the tour title, linked to the tour page, and the directions. - The map shows all tours at once. Scroll-wheel zoom only starts after clicking into the map, so the page still scrolls normally. - The map is only created when it scrolls near the visible part of the page, so visitors who don't scroll down load no map tiles. That also means the accessibility check (pa11y) doesn't see the map: it would otherwise report the text above the map, because axe wrongly takes the clipped map tiles to overlap it. Check changes to the map's markers and controls by hand, e.g. with the axe browser extension. - Without JavaScript, and for screen readers, it shows a list of the tours with their directions and a link to each location on openstreetmap.org. - If a text is missing in the page's language, the other language is used. - Without any tours with `geo`, the shortcode renders nothing and the build logs a warning with the ID `warning-tour-map-empty`. ### Map data in the schedule file A map of an event shows where each tour takes place and how to get there from the Congress. The data for it goes into the same file as the schedule, `data/schedule/.yaml`, next to `days`. Like the schedule, it is shared by both languages; anything that people read comes with a `de` and an `en` version. ```yaml map: origin: # where all routes start, shown with its own marker name: { de: "Congress (CCH)", en: "Congress (CCH)" } geo: { lat: 53.561450, lon: 9.986360 } icon: computer tours: miniatur-wunderland: # tour slug, as in days[].tours[].tour geo: { lat: 53.543843, lon: 9.988765 } color: "#32cd32" icon: tram-stop directions: de: | - Ab S Dammtor mit dem Bus 4 Richtung Osakaallee. - Am Rathausmarkt in die U3 Richtung Schlump umsteigen. - An der U Baumwall aussteigen, über den Kanal, links am Kanal entlang bis zum Eingang. en: | - From S Dammtor take bus 4 towards Osakaallee. - At Rathausmarkt change to the U3 towards Schlump. - Exit at U Baumwall, cross the canal and walk left alongside it to the entrance. route: # from map.origin to geo, one entry per leg - { mode: walk, polyline: "…" } - { mode: bus, line: "4", polyline: "…" } - { mode: subway, line: "U3", polyline: "…" } - { mode: walk, polyline: "…" } days: # … as described under "Shortcode: Tour schedule" ``` `tours` has one entry per tour slug that should appear on the map. A slug can be in `tours` without being scheduled, and the other way round; scheduled tours without an entry simply don't appear on the map. | Field | Required | Content | |---|---|---| | `geo` | yes | Meeting point or venue: `lat` and `lon` in decimal degrees (WGS 84), six decimals are plenty. Write it even though the tour page has the same value in `params.geo`; the map only reads this file. | | `color` | no | Colour of the marker and the route, as a CSS colour (`"#32cd32"` or `LimeGreen`). Pick colours that differ between tours. | | `icon` | no | Name of an icon for the marker, see "Map icons" below. Without it, or with an unknown name, the marker is a plain coloured circle; an unknown name also logs a build warning. | | `directions` | no | How to get there from the origin, per language (`de`, `en`) as Markdown, usually a short list of steps. | | `route` | no | The way from `map.origin` to `geo` as a list of legs in travel order. Without it the tour only gets a marker. | | `duration` | no | Length of the tour in minutes, used by the calendar entries when the `calendar` shortcode doesn't pass one. | Each leg in `route` has: - `mode`: `walk`, `bus`, `subway` (U-Bahn), `train` (S-Bahn, regional train), `ferry` or `other`. The map can draw walking legs differently from rides. - `line` (optional): the line name as signposted, for example `"4"`, `"U3"` or `"S2"`. - The geometry, in one of two forms: - `polyline`: an [encoded polyline](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) as returned by most routing services. Add `precision: 6` if it was encoded with six decimals instead of the default five. - `coordinates`: a list of `[lon, lat]` pairs (GeoJSON order), handy for hand-drawn routes exported from uMap or geojson.io. Legs should join up: each leg starts where the previous one ends, and the first leg starts at `map.origin`. `map.origin` is usually the same for all tours of an event, so it is written once. It takes `name` (per language), `geo` and an optional `icon`. ### Map icons Marker icons come from three public-domain (CC0) icon sets for maps, kept in [assets/vendor/icons/](assets/vendor/icons/): | Set | Icons | Browse | |---|---|---| | `osmic` | 154 | [OSMIC](https://github.com/gmgeo/osmic), or the files in `assets/vendor/icons/osmic/` | | `maki` | 211 | [Maki](https://labs.mapbox.com/maki-icons/) | | `temaki` | 557 | [Temaki](https://rapideditor.github.io/temaki/docs/) | `icon` takes the file name without `.svg`, for example `ferry` or `railway-halt`. The name is looked up in `osmic`, then `maki`, then `temaki`; some names exist in more than one set, so to pick a specific set, put its name in front: `maki:garden`, `temaki:binoculars`. uMap's pictograms are OSMIC and Maki icons with the same names, and this lookup order picks the same icon as uMap for every one of them, so `icon` values copied from uMap work unchanged. Only the icons a page uses end up in it, so the size of the sets doesn't matter. To update a set, replace its folder with the SVG files of a newer version; each folder has a README with the version it came from. ### The 39C3 tour map (uMap) The 39C3 overview pages embed a map with the `map-39c3` shortcode ([layouts/_shortcodes/map-39c3.html](layouts/_shortcodes/map-39c3.html)). It is an iframe of the uMap map [39c3 Hackertours](https://umap.openstreetmap.fr/en/map/39c3-hackertours_1329184), which was created by the uMap user WinstonS in December 2025. The map has no other editors, so only that account can change it. Nothing about the map is stored in this repository apart from the embed code. How the map is built: - The background is the "Jawg Dark" tile layer. - Layer 1 holds a single marker for the Congress (CCH). - Each tour has its own layer, named after the tour (`Alsterdampfer`, `MiWuLa`, `XFEL`, …), with two features of the same colour: - A marker at the venue, with a pictogram icon. Its name is an HTML link to the English tour page, and its description lists the directions from the Congress (English only). - A line named "Way to …" from the Congress area (mostly S Dammtor) to the venue, with the same directions as its description. - The lines were most likely drawn by hand with uMap's line tool rather than calculated by a routing service: most of them start near S Dammtor, but each at a slightly different point, the vertices are spaced irregularly (30 to 100 m on average, up to several hundred metres), and the XFEL route consists of two separately drawn pieces. The routes combine walking, bus, U-Bahn and S-Bahn. To change the map, edit it on uMap (as the owner, or after the owner adds you as an editor); the website picks up changes immediately because the iframe loads the live map. When adding a layer, its ID has to be added to the `datalayers` parameter in the embed URL, which uMap's "Share" panel generates. The uMap data has been converted into this format in [data/schedule/39c3.yaml](data/schedule/39c3.yaml), with English directions only and each route as one leg of mode `other`. To get the data out again, for example after changes on uMap: - A single layer as GeoJSON: `https://umap.openstreetmap.fr/en/datalayer/1329184//`. The layer IDs are listed in the `datalayers` parameter of the embed URL. - The whole map: the "Download" option in uMap's share panel exports all layers as one GeoJSON file. In the GeoJSON, `coordinates` are already in `[lon, lat]` order, the colour and icon are in each feature's `properties._umap_options`, and the directions are in `properties.description`. ## Shortcode: Image gallery `{{< gallery >}}` shows a page's photos as square thumbnails, two per row on wide screens and one per row on narrow ones. Clicking a thumbnail opens the full image in a lightbox with its alt text and credit; with more than one image, arrows (on screen, the arrow keys, or swiping) move between them, and Escape, the close button or a click beside the image closes it. ```markdown {{< gallery >}} {{< gallery-image src="hafenrundfahrt1.jpg" alt="Containerschiffe „HMM Dublin“ und „Ever Greet“ im Hafen" credit="© Wikipedia-User CC BY-SA 4.0 Shotbyp4ul" >}} {{< gallery-image src="hafenrundfahrt2.jpg" alt="Das Speicherstadtrathaus" credit="© Wikipedia-User CC BY-SA 3.0 Magnus Manske" >}} {{< /gallery >}} ``` | Param | Required | Content | |---|---|---| | `src` | yes | The image: a file in the page's folder, or a path in `assets/`. It must be a photo Hugo can resize (JPEG, PNG, WebP…), not SVG. | | `alt` | yes | What the image shows. It is the thumbnail's alt text and the caption in the lightbox. | | `credit` | no | Photographer and licence, shown under the thumbnail and in the lightbox. | Hugo crops the thumbnails to squares (keeping the most interesting part of the picture) at build time; the lightbox shows the original file. Without JavaScript, a thumbnail links to the original file. A missing image, a missing `alt` or a `gallery-image` outside a `gallery` stops the build with an error that names the page. ## Accessibility check CI runs [pa11y-ci](https://github.com/pa11y/pa11y-ci) with the axe and HTML_CodeSniffer rules for WCAG 2 AA on every push and pull request, in its own job (`accessibility`), so findings show up without blocking the deploy. To run the same check locally (needs Node.js, Python 3 and Chrome or Chromium): ```shell ./scripts/pa11y.sh ``` The script builds the site, serves it on `localhost:8080` and checks every page in the sitemaps, except the past events and the individual news posts, which are kept as they were. The list of skipped sections is `EXCLUDE` in [scripts/pa11y.sh](scripts/pa11y.sh); the rules are in [.pa11yci.json](.pa11yci.json). pa11y only checks the light colour scheme. Some things to keep in mind when writing pages: - Give every image an alt text that says what it shows. The image title is the photo credit (`![Alt text](photo.jpg "© Photographer")`); it is shown below the image. - Headings in a page start at `##`; `#` is the page title. - Don't put information only into images, like directions on a map; write it out as text as well.