Skip to content
Merged
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
45 changes: 0 additions & 45 deletions .github/workflows/accessibility.yml

This file was deleted.

86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
pull_request:
paths:
- 'templates/**'
- 'static/**'
- 'content/**'
push:

permissions: {}

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6.0.1

- name: Build Zola
uses: getzola/github-pages@066755243e69f508fd1a74739fbf1a65f656c790
with:
zola_version: v0.22.1
check_links: false
check_flags: "--skip-external-links"

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: public
path: public/
retention-days: 1

axe-core:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: public
path: public/

- name: Set up bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "latest"

- name: Install chrome with browser-driver-manager
run: bunx browser-driver-manager@2.0.1 install chrome

- name: Serve site and run axe-core
run: |
python3 -m http.server 8000 --directory public &
sleep 2
bunx @axe-core/cli@4.11.1 http://localhost:8000/ \
http://localhost:8000/providers/ \
http://localhost:8000/providers/scaleway/ \
http://localhost:8000/compare.html?providers=aws,google-cloud,microsoft-azure --exit

- name: Report results
if: always()
run: echo "Accessibility testing completed. Check logs above for any violations."

lighthouse:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6.0.1

- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: public
path: public/

- name: Set up bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "latest"

- name: Lighthouse analysis
run: bunx @lhci/cli@0.15.x autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ node_modules/
package.json
bun.lock

.lighthouseci/
26 changes: 26 additions & 0 deletions .lighthouserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"ci": {
"collect": {
"staticDistDir": "./public"
},
"assert": {
"assertions": {
"categories:performance": ["warn", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"categories:best-practices": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.95 }],

"network-dependency-tree-insight": "off",

"render-blocking-resources": ["warn", { "maxLength": 1 }],
"render-blocking-insight": ["warn", { "maxLength": 1 }],

"label-content-name-mismatch": ["error", { "minScore": 0.9 }],
"html-has-lang": ["error", { "minScore": 1 }],
"meta-description": ["error", { "minScore": 1 }],
"viewport": ["error", { "minScore": 1 }],
"errors-in-console": ["error", { "minScore": 0.9 }]
}
}
}
}
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cloudlandscape/cloudlandscape.github.io/badge)](https://scorecard.dev/viewer/?uri=github.com/cloudlandscape/cloudlandscape.github.io)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12157/badge)](https://www.bestpractices.dev/projects/12157)
[![CodeQL Advanced](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/codeql.yml/badge.svg)](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/codeql.yml)
[![Accessibility Testing](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/accessibility.yml/badge.svg)](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/accessibility.yml)
[![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/cloudlandscape/cloudlandscape.github.io?style=for-the-badge&label=OpenSSF%20Scorecard)](https://scorecard.dev/viewer/?uri=github.com/cloudlandscape/cloudlandscape.github.io)
[![OpenSSF Best Practices](https://img.shields.io/cii/percentage/12157?style=for-the-badge&label=OpenSSF%20Best%20Practices)](https://www.bestpractices.dev/en/projects/12157/passing)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/cloudlandscape/cloudlandscape.github.io/codeql.yml?style=for-the-badge&label=CodeQL)](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/codeql.yml)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/cloudlandscape/cloudlandscape.github.io/accessibility.yml?style=for-the-badge&label=Web%20tests)](https://github.com/cloudlandscape/cloudlandscape.github.io/actions/workflows/ci.yml)
[![GitHub License](https://img.shields.io/github/license/cloudlandscape/cloudlandscape.github.io?style=for-the-badge&color=blue)](https://eupl.eu/)

# Cloud landscape

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Story 3.6: Screen Reader Support for All Content

Status: done

## Story

As a **blind or low-vision user relying on screen readers**,
I want all content and functionality to be announced correctly,
So that I can independently navigate and use the site.

## Acceptance Criteria

1. **Heading hierarchy**: All page headings announced with proper h1→h2→h3 hierarchy
2. **Landmarks**: Navigation, main, and contentinfo (footer) landmarks present on every page
3. **Form labels**: Every input has an associated `<label>` before the field
4. **Filter state**: Active filter checkbox state readable; result count live-region announces updates
5. **Result count**: `aria-live="polite"` on result count — screen reader hears updates
6. **Button purposes**: All buttons have descriptive `aria-label` ("Add Scaleway to Comparison", etc.)
7. **Link destinations**: All links have meaningful text or `aria-label`
8. **Comparison table**: Semantic `<caption>`, `scope` attributes on all `<th>` cells; ✓/✗ symbols have text alternatives readable by screen readers
9. **Dynamic content**: `#comparison-content` has `aria-live="polite"` — screen reader hears when table loads
10. **Certifications section** on provider detail page: `<section>` has `aria-labelledby` pointing to its heading

## Tasks / Subtasks

- [x] Task 1 — Add `<footer>` landmark to `base.html` (AC: #2)
- [x] 1.1 Add `<footer>` element with role="contentinfo" (implicit on footer) after `<main>` in `base.html`
- [x] 1.2 Footer content: minimal — copyright + link to home, wrapped in `<p>` inside `<footer>`

- [x] Task 2 — Fix comparison table accessibility in `compare.html` (AC: #8, #9)
- [x] 2.1 Add `aria-live="polite"` and `aria-busy="false"` to `<div id="comparison-content">`; set `aria-busy="true"` while loading, revert to `"false"` after render
- [x] 2.2 Add `<caption>` to the comparison table in the `renderComparison()` function in compare.html
- [x] 2.3 Add `scope="col"` to all column header `<th>` in the thead row
- [x] 2.4 Add `scope="row"` to all row header `<th>` (Feature, Country, service names, cert names, Datacenters)
- [x] 2.5 Add `scope="colgroup"` to section header rows (Services, Certifications, Geographic Coverage colspan rows)
- [x] 2.6 Replace `✓`/`✗` symbols with `<span aria-hidden="true">✓</span><span class="sr-only">Yes</span>` and equivalent for No

- [x] Task 3 — Fix certifications section in `page.html` (AC: #10)
- [x] 3.1 Add `id="certifications-heading"` to `<h2>Certifications & Compliance</h2>`
- [x] 3.2 Add `aria-labelledby="certifications-heading"` to the certifications `<section>` element

- [x] Task 4 — Verify and validate (AC: all)
- [x] 4.1 Run `mise run build` — ✅ 26 pages, 0 errors
- [x] 4.2 Run `mise run check` (zola check) — ✅ 26 pages, 0 errors
- [x] 4.3 Run `mise run a11y` (axe-core) — ✅ 0 violations on all 4 pages

## Dev Notes

### Current State — What Already Works ✅

- **Heading hierarchy**: h1 on every page; h2 on provider cards; h1→h2→h3 on taxonomy pages ✅
- **`<nav aria-label>`**: Main navigation landmark on all pages ✅
- **`<main id="main-content">`**: Main landmark on all pages (base.html + compare.html) ✅
- **Form labels**: `<label for="search-input">` + `<label><input>` wrap pattern on all checkboxes ✅
- **`aria-live="polite"`**: On `.result-count` in section.html ✅
- **`<fieldset>` + `<legend>`**: All 3 filter groups wrapped ✅
- **Button aria-labels**: "Add X to Comparison" buttons have `aria-label="Add {{ page.title }} to comparison"` ✅
- **External link notice**: `aria-label="(opens in new tab)"` on external link icon ✅
- **`aria-hidden`**: Decorative SVG logo in base.html ✅
- **`aria-labelledby`**: Provider info, services, and datacenter sections in page.html ✅

### Gaps to Fix 🔧

1. **No `<footer>` landmark** — base.html ends with `<script>` tags, no `contentinfo` landmark. Required by WCAG 4.1.2 and RGAA criterion 12.6.

2. **compare.html table missing semantics**:
- No `<caption>` on comparison table — screen readers announce table with no context
- No `scope` attributes on any `<th>` — column/row relationships unknown to AT
- Section divider rows (`<th colspan="">Services</th>`) have no `scope="colgroup"`
- ✓/✗ symbols (`✓`, `✗`) are rendered without accessible text alternatives — SR announces them as Unicode characters, not meaningful Yes/No

3. **`#comparison-content` has no `aria-live`** — when JS renders the table (or the "no providers selected" message), screen readers receive no notification.

4. **Certifications section in page.html**: `<section class="provider-certifications">` has no `aria-labelledby` — unlike the services, datacenter, and about sections which all have it.

### Key File Locations

- `templates/base.html` — global layout; add footer before closing `</body>`
- `static/compare.html` — standalone static file; modify `#comparison-content` div + `renderComparison()` JS function
- `templates/page.html` — provider detail template; fix certifications section (~line 72)
- `static/style.css` — may need `.sr-only` utility class (check if already present)

### `.sr-only` CSS Pattern

Must verify `.sr-only` exists in `style.css`. If not, add:
```css
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
```

### Table Accessibility Pattern

For the comparison table generated by `renderComparison()` in `compare.html`:
```html
<table class="comparison-table">
<caption>Provider feature comparison</caption>
<thead>
<tr>
<th scope="col">Feature</th>
<th scope="col"><a href="/providers/scaleway/">Scaleway</a></th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr><th scope="row">Country</th><td>France</td>...</tr>
<tr><th colspan="N" scope="colgroup">Services</th></tr>
<tr><th scope="row">Compute</th><td>...</td></tr>
<!-- ... -->
</tbody>
</table>
```

For ✓/✗ cells:
```html
<td><span aria-hidden="true">✓</span><span class="sr-only">Yes</span></td>
<td><span aria-hidden="true">✗</span><span class="sr-only">No</span></td>
```

### Architecture References

- [Source: project-context.md#Accessibility Rules] — RGAA 4 AA, axe-core CI
- [Source: project-context.md#JavaScript Rules] — vanilla JS only, `aria-live="polite"` on dynamic containers
- WCAG 2.1 SC 1.3.1 (Info and Relationships), SC 4.1.2 (Name, Role, Value)

## Dev Agent Record

### Agent Model Used

claude-sonnet-4.6

### Debug Log References

### Completion Notes List

### File List

- `templates/base.html` — Add `<footer>` landmark
- `static/compare.html` — Add `aria-live` to content div; fix table semantics (caption, scope, sr-only for ✓/✗); add `<footer>` landmark; fix `scope="colgroup"` on section divider rows; replace inline error style with CSS class
- `templates/page.html` — Add `aria-labelledby` to certifications section; full i18n pass
- `static/style.css` — Add `.sr-only` utility class, `.site-footer` styles, `.comparison-error` class
- `zola.toml` — Add 14 i18n keys EN+FR (footer_copyright, provider_info_label, country_of_origin, headquarters_label, founded_label, website_label, opens_in_new_tab, about_heading, services_heading, services_intro, certifications_heading, certifications_intro, view_attestation, datacenter_heading, datacenter_intro, back_to_providers)
2 changes: 1 addition & 1 deletion _bmad-output/implementation-artifacts/sprint-status.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ development_status:
3-3-display-ui-navigation-and-elements-in-both-languages: done
3-4-persist-user-language-preference: done
3-5-keyboard-navigation-for-all-features: done
3-6-screen-reader-support-for-all-content: backlog
3-6-screen-reader-support-for-all-content: done
3-7-sufficient-color-contrast-wcag-2-1-aa: done # Merged via PR #7 (brand identity)
3-8-provide-skip-navigation-links: backlog
3-9-use-semantic-html-structure: backlog
Expand Down
7 changes: 6 additions & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ EXIT_CODE=$?
exit $EXIT_CODE
"""

[tasks.lighthouse]
description = "Run Lighthouse analysis"
depends = ["build"]
run = "bunx @lhci/cli@0.15.x autorun"

[tasks.test]
description = "Run full tests"
depends = ["validate", "check", "a11y"]
depends = ["validate", "check", "a11y", "lighthouse"]

Loading
Loading