diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index db47f8e..ec8cb71 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -78,3 +78,26 @@ jobs: with: name: website-build path: public/ + + # Separate from the build, so that accessibility findings show up on PRs without blocking deploys. + accessibility: + runs-on: docker + container: + image: docker.io/hugomods/hugo:0.165.0 + steps: + - name: Install packages + run: | + apk update + # For CI actions and pa11y. + apk add nodejs-current npm + # For website build. + apk add tzdata git + # For pa11y: a browser, fonts for it, and a web server for the built site. + apk add chromium font-dejavu python3 + + - uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Check accessibility with pa11y + run: ./scripts/pa11y.sh diff --git a/.pa11yci.json b/.pa11yci.json new file mode 100644 index 0000000..8fb6192 --- /dev/null +++ b/.pa11yci.json @@ -0,0 +1,10 @@ +{ + "defaults": { + "standard": "WCAG2AA", + "runners": ["axe", "htmlcs"], + "timeout": 60000, + "chromeLaunchConfig": { + "args": ["--no-sandbox", "--disable-dev-shm-usage"] + } + } +} diff --git a/README.md b/README.md index 0d41ddf..8d52f59 100644 --- a/README.md +++ b/README.md @@ -149,3 +149,19 @@ The uMap data has been converted into this format in [data/schedule/39c3.yaml](d - 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`. + +## 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. diff --git a/assets/css/_custom.css b/assets/css/_custom.css index 992c2dc..6c63a8f 100644 --- a/assets/css/_custom.css +++ b/assets/css/_custom.css @@ -2,6 +2,7 @@ @import './_tour-map.css'; .header__intro_headline { + margin: 0; font: bold var(--fs-xxl) "Arial"; padding-bottom: 10px; } @@ -33,6 +34,7 @@ th, td { --color-header-bg: #242424; --color-text: #faf5f5; --color-link: #66f2ff; + --color-link-visited: #66f2ff; --color-footer-bg: #242424; --color-border: #faf5f5; --color-button: #141414; @@ -50,7 +52,9 @@ th, td { .main-menu a:active { text-decoration: none; } - a { + /* Links in text keep their underline: the link colour alone is too close to the text colour. */ + nav a, + .header a { text-decoration: none; } } @@ -61,4 +65,44 @@ h2 a { .title-post-date { margin-left: 0em; -} \ No newline at end of file +} +/* Visible when focused with the keyboard; the visually-hidden class hides it otherwise. */ +.skip-link:focus { + position: absolute; + inset-block-start: 0.5rem; + inset-inline-start: 0.5rem; + z-index: 100; + padding: 0.5rem 1rem; + background: var(--color-button); + color: var(--color-button-text); +} + +#main-content:focus { + outline: none; +} + +/* Content images with their credit, see layouts/_markup/render-image.html. */ +.content-image { + margin: 1rem 0; + + img { + display: block; + max-width: 100%; + height: auto; + } +} + +span.content-image { + display: inline-block; + vertical-align: top; + max-width: 100%; + margin: 0 0.5rem 0.5rem 0; +} + +.content-image__credit { + display: block; + margin-block-start: 0.25rem; + font-family: var(--ff-alt); + font-size: var(--fs-xs); + color: var(--color-text-meta); +} diff --git a/assets/css/_schedule.css b/assets/css/_schedule.css index c9f51f6..66d514b 100644 --- a/assets/css/_schedule.css +++ b/assets/css/_schedule.css @@ -68,16 +68,16 @@ } } -/* Lined up with the time on the cards. */ +/* Lined up with the time on the cards, but in normal weight to stay in the background. + No transparency: it has to keep text contrast. */ .schedule__hour-label { display: block; padding: calc(0.5rem + 0.25rem) 0.75rem 0.5rem calc(0.75rem + 3px); font-family: var(--ff-alt); font-size: var(--fs-s); - font-weight: bold; + font-weight: normal; font-variant-numeric: tabular-nums; color: var(--color-text-meta); - opacity: 0.45; } .schedule__day-title { @@ -177,6 +177,11 @@ } @container (width < 48rem) { + /* Keeps a focused card from scrolling under the sticky day picker. */ + .schedule__title { + scroll-margin-block-start: 6rem; + } + .schedule__picker:not([hidden]) { position: sticky; inset-block-start: 0; diff --git a/content/de/_index.md b/content/de/_index.md index 739cb83..3b386b4 100644 --- a/content/de/_index.md +++ b/content/de/_index.md @@ -15,8 +15,6 @@ cascade: intro: Wir von Hamburg Hackertours zeigen euch die Stadt! --- -## Willkommen bei Hamburg Hackertours! - Für den [40C3](https://events.ccc.de/category/40c3/) haben wir interessante Orte in und um Hamburg gefunden, an denen ihr für ein paar Stunden in andere Welten eintauchen und spannende Dinge erleben könnt. [Wie das funktioniert, erklären wir euch hier in unseren FAQ]({{< relref "faq/_index" >}})! HackerTours ist auch auf **[Mastodon](https://chaos.social/@hackertours)**! @@ -31,4 +29,4 @@ Hackertours-Desk: DECT 8687 "TOUR", +49 40 573 0830 8687 Frühere Veranstaltungen: [37C3](37c3) / [38C3](38c3) / [EH22](eh22) / [39C3](39c3) -# News +## News diff --git a/content/en/_index.md b/content/en/_index.md index 8f75b3a..9a1cdb0 100644 --- a/content/en/_index.md +++ b/content/en/_index.md @@ -15,8 +15,6 @@ cascade: intro: Let Hamburg Hackertours show you around the city! --- -## Welcome to Hamburg Hackertours! - For [40C3](https://events.ccc.de/category/40c3/) we have found interesting places in and around Hamburg where you can delve into other worlds and experience fascinating things for a few hours. [We are going to explain how this works here in our FAQ article]({{< relref "faq/_index" >}})! HackerTours is also on **[Mastodon](https://chaos.social/@hackertours)**! @@ -31,5 +29,5 @@ Hackertours Desk: DECT 8687 "TOUR", +49 40 573 0830 8687 Previous events: [37C3](37c3) / [38C3](38c3) / [EH22](eh22) / [39C3](39c3) -# News +## News \ No newline at end of file diff --git a/hugo.yaml b/hugo.yaml index 30ef755..875dc45 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -44,3 +44,9 @@ languages: pagination: pagerSize: 25 title: "Hamburg Hackertours" + +markup: + goldmark: + parser: + # Lets the image render hook tell images on their own line (.IsBlock) from inline ones. + wrapStandAloneImageWithinParagraph: false diff --git a/i18n/de.yaml b/i18n/de.yaml index 7506009..8858304 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -2,3 +2,6 @@ schedule_day: "Tag {{ .Day }}" schedule_pick_day: "Tag auswählen" tour_map_label: "Karte der Tourorte" tour_map_osm: "auf OpenStreetMap" +skip_to_content: "Zum Inhalt springen" +logo_alt: "Hamburg Hackertours – Startseite" +map_39c3_title: "Karte der 39C3-Touren mit Wegen vom CCH (uMap)" diff --git a/i18n/en.yaml b/i18n/en.yaml index 029dab9..ef3ea3c 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -2,3 +2,6 @@ schedule_day: "Day {{ .Day }}" schedule_pick_day: "Choose a day" tour_map_label: "Map of the tour locations" tour_map_osm: "on OpenStreetMap" +skip_to_content: "Skip to content" +logo_alt: "Hamburg Hackertours – home page" +map_39c3_title: "Map of the 39C3 tours with routes from the CCH (uMap)" diff --git a/layouts/_markup/render-image.html b/layouts/_markup/render-image.html new file mode 100644 index 0000000..9c7d95f --- /dev/null +++ b/layouts/_markup/render-image.html @@ -0,0 +1,30 @@ +{{- /* +Content images. The Markdown title ("![alt](src "© Credit")") is the photo credit; it is shown +as a caption instead of a tooltip, so that it is available on touch devices and to screen readers. +An image on its own line becomes a figure; images sharing a paragraph get an inline credit. +*/ -}} +{{- $src := .Destination -}} +{{- $width := 0 -}} +{{- $height := 0 -}} +{{- with or (.PageInner.Resources.Get (strings.TrimPrefix "./" .Destination)) (resources.Get .Destination) -}} + {{- $src = .RelPermalink -}} + {{- if and (eq .ResourceType "image") (ne .MediaType.SubType "svg") -}} + {{- $width = .Width -}} + {{- $height = .Height -}} + {{- end -}} +{{- end -}} +{{- /* A title that only repeats the alt text is not a credit. */ -}} +{{- $credit := cond (eq .Title .PlainText) "" .Title -}} +{{- $img := printf `%s` (htmlEscape $src) (htmlEscape .PlainText) (cond (gt $width 0) (printf ` width="%d" height="%d"` $width $height) "") -}} +{{- if .IsBlock -}} +
+{{ $img | safeHTML }} +{{- with $credit }} +
{{ . }}
+{{- end }} +
+{{- else if $credit -}} +{{ $img | safeHTML }}{{ $credit }} +{{- else -}} +{{ $img | safeHTML }} +{{- end -}} diff --git a/layouts/_partials/lang-badge.html b/layouts/_partials/lang-badge.html index a305135..7698e49 100644 --- a/layouts/_partials/lang-badge.html +++ b/layouts/_partials/lang-badge.html @@ -1,21 +1,25 @@ -{{- /* Language badge for a tour. Params: code ("de", "en" or "deen"), class (optional; without it the badge is styled as an inline superscript). */ -}} +{{- /* Language badge for a tour. Params: code ("de", "en" or "deen"), class (optional; without it the badge is styled as an inline superscript). +The visible label ("DE/EN") is hidden from screen readers, which read a short text instead; the full sentence is a tooltip. */ -}} {{- $code := .code | lower -}} {{- $validCodes := slice "de" "en" "deen" -}} {{- if not (in $validCodes $code) -}} Warning: Invalid or missing language code '{{ $code }}'. Use 'de', 'en', or 'deen'. {{- else -}} - {{- $label := cond (eq $code "de") "DE" (cond (eq $code "en") "EN" (cond (eq $code "deen") "DE/EN" "")) -}} + {{- $de := eq site.Language.Name "de" -}} + {{- $label := cond (eq $code "de") "DE" (cond (eq $code "en") "EN" "DE/EN") -}} {{- $title := "" -}} + {{- $spoken := "" -}} {{- if eq $code "deen" -}} - {{- $title = cond (eq site.Language.Name "de") "Diese Tour wird zweisprachig stattfinden (Deutsch und Englisch)" "This tour will be bilingual (German and English)" -}} + {{- $title = cond $de "Diese Tour wird zweisprachig stattfinden (Deutsch und Englisch)" "This tour will be bilingual (German and English)" -}} + {{- $spoken = cond $de "Sprache: Deutsch und Englisch" "Language: German and English" -}} {{- else if eq $code "de" -}} - {{- $title = cond (eq site.Language.Name "de") "Diese Tour wird auf Deutsch stattfinden" "This tour will be held in German" -}} + {{- $title = cond $de "Diese Tour wird auf Deutsch stattfinden" "This tour will be held in German" -}} + {{- $spoken = cond $de "Sprache: Deutsch" "Language: German" -}} {{- else if eq $code "en" -}} - {{- $title = cond (eq site.Language.Name "de") "Diese Tour wird auf Englisch stattfinden" "This tour will be held in English" -}} - {{- end -}} - {{- with .class -}} - {{ $label }} - {{- else -}} - {{ $label }} + {{- $title = cond $de "Diese Tour wird auf Englisch stattfinden" "This tour will be held in English" -}} + {{- $spoken = cond $de "Sprache: Englisch" "Language: English" -}} {{- end -}} + {{- $style := "font-size: .65em; vertical-align: super;" -}} + {{- with .class }}{{ $style = "" }}{{ end -}} + {{ $spoken }} {{- end -}} diff --git a/layouts/_shortcodes/map-39c3.html b/layouts/_shortcodes/map-39c3.html index 34999ba..ffe1f50 100644 --- a/layouts/_shortcodes/map-39c3.html +++ b/layouts/_shortcodes/map-39c3.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/layouts/baseof.html b/layouts/baseof.html index ed6133b..6e6bf60 100644 --- a/layouts/baseof.html +++ b/layouts/baseof.html @@ -18,15 +18,16 @@ +
{{ if $.Param "logo" | default true -}} - {{ end -}}
- +
{{ .Param "intro" }}
@@ -38,6 +39,7 @@ {{ if not ($.Param "menuinheader") }}{{ partial "menu.html" . }}{{ end -}} {{ if $.Param "mobilemenu" }}{{ partial "mobilemenu.html" . }}{{ end -}} +
{{ block "main" . }}{{ end }} {{ if $.Param "sidebar" }}{{ partial "sidebar.html" . }}{{ end -}} diff --git a/layouts/home-baseof.html b/layouts/home-baseof.html index a8ae177..156a130 100644 --- a/layouts/home-baseof.html +++ b/layouts/home-baseof.html @@ -18,15 +18,16 @@ +
{{ if $.Param "logo" | default true -}} - {{ end -}}
-
{{ .Param "headline" }}
+

{{ .Param "headline" }}

{{ if .Param "intro" }}
{{ .Param "intro" }}
{{ end }} @@ -40,6 +41,7 @@ {{ if not ($.Param "menuinheader") }}{{ partial "menu.html" . }}{{ end -}} {{ if $.Param "mobilemenu" }}{{ partial "mobilemenu.html" . }}{{ end -}} +
{{ block "main" . }}{{ end }} {{ if $.Param "sidebar" }}{{ partial "sidebar.html" . }}{{ end -}} diff --git a/scripts/pa11y.sh b/scripts/pa11y.sh new file mode 100755 index 0000000..e8bfece --- /dev/null +++ b/scripts/pa11y.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Accessibility check with pa11y-ci, used by CI and for local runs (see README.md). +# Builds the site into a temporary directory, serves it on localhost and checks every page +# in the sitemaps except the past events, which are kept as they were. +set -eu + +PA11Y_CI_VERSION=5.0.0 +PORT=${PORT:-8080} +# Pages that are not checked: past events and the individual news posts. +EXCLUDE='^http://localhost:[0-9]+/[a-z]+/(37c3|38c3|39c3|eh22|posts/[^/]+)/' + +# Use an installed Chrome/Chromium instead of letting Puppeteer download one. +if [ -z "${PUPPETEER_EXECUTABLE_PATH:-}" ]; then + for candidate in /usr/bin/chromium-browser /usr/bin/chromium "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; do + if [ -x "$candidate" ]; then PUPPETEER_EXECUTABLE_PATH=$candidate; break; fi + done +fi +export PUPPETEER_EXECUTABLE_PATH +export PUPPETEER_SKIP_DOWNLOAD=1 + +cd "$(dirname "$0")/.." +site=$(mktemp -d) +hugo --quiet --buildFuture --buildDrafts --baseURL "http://localhost:$PORT/" -d "$site" +python3 -m http.server "$PORT" --bind 127.0.0.1 -d "$site" >/dev/null 2>&1 & +server=$! +trap 'kill $server; rm -rf "$site"' EXIT +sleep 1 + +status=0 +for lang in de en; do + npx --yes "pa11y-ci@$PA11Y_CI_VERSION" --config .pa11yci.json \ + --sitemap "http://localhost:$PORT/$lang/sitemap.xml" --sitemap-exclude "$EXCLUDE" || status=1 +done +exit $status