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 (``); 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 ("") 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 `` (htmlEscape $src) (htmlEscape .PlainText) (cond (gt $width 0) (printf ` width="%d" height="%d"` $width $height) "") -}}
+{{- if .IsBlock -}}
+