Fix duplicate section heading IDs by jay23606 · Pull Request #83 · philorien/dcma-site · GitHub
Skip to content

Fix duplicate section heading IDs - #83

Merged
philorien merged 3 commits into
philorien:mainfrom
jay23606:pr-scout/80-a11y-duplicate-heading-ids-on-projects-m
Oct 16, 2026
Merged

philorien merged 3 commits into
philorien:mainfrom
jay23606:pr-scout/80-a11y-duplicate-heading-ids-on-projects-m

Conversation

@jay23606

Copy link
Copy Markdown
Contributor

Bug

Repeated prose, split, and activity sections reused hardcoded heading IDs, so their aria-labelledby references could resolve to the wrong heading.

Change

Generate a per-instance heading ID with Vue's useId() and use it for each section's heading and aria-labelledby reference.

Testing

  • node node_modules/vitest/vitest.mjs run tests/sectionHeadingIds.test.ts
  • git diff --check

Regression test: fails on base twice (/expected 2 to be 1/), passes on fix twice (node node_modules/vitest/vitest.mjs run tests/sectionHeadingIds.test.ts)

Fixes #80

@vercel

vercel Bot commented Oct 13, 2026

Copy link
Copy Markdown

@philorien philorien left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. The useId() approach is the right fix and is SSR-stable on Nuxt 4 / Vue 3.5. A few things need to change before merge, though: the fix is incomplete for #80, and one rewritten line introduces a new a11y problem. Inline comments below cover the files in this diff. Three issues are in files this PR doesn't touch, so they're listed here instead:

1. app/components/ContactSection.vue (~line 79): still has hardcoded IDs. ContactSection is rendered once per contactSectionBlock by PageSections, the same as the components fixed here, but it still uses id="contact-heading" and err-firstName, err-email, etc. With two contact blocks on one page, both sections are labelled by the first heading, and the second form's aria-describedby points at the first form's error spans, so screen readers announce the wrong errors. Please switch these to useId() as well (one base ID per instance, e.g. ${id}-err-email).

2. app/components/fridge/FridgeClosingCta.vue (~line 7): still has closing-heading. This is rendered per ctaSection by PageSections and also directly on full-hearts-fridge.vue. Two CTA blocks (or the fridge page plus a CMS CTA) produce duplicate id="closing-heading", which is exactly the bug #80 describes. Same useId() fix applies.

3. app/components/home/ActivityGrid.vue lines 23-25: invalid void-element closing tags. <br></br> and <hr></hr> aren't valid HTML. Browsers parse </br> as a second <br>, so each one produces two line breaks instead of one, and </hr> is a parse error. Please replace these with spacing CSS (e.g. margin on .items / .btn) or a single <hr>.

Also, app/components/page/PageSections.vue:42 passes section.heading || '' to HomeActivityGrid; see the inline comment on ActivityGrid.vue for that one.

Comment thread app/components/page/PageProseSection.vue Outdated
Comment thread app/components/home/ActivityGrid.vue Outdated
Comment thread tests/sectionHeadingIds.test.ts
@philorien

Copy link
Copy Markdown
Owner

@jay23606 thanks for the update. I re-reviewed 42ad06f: the ContactSection and FridgeClosingCta IDs, the conditional aria-labelledby on prose, the optional grid heading and the new tests all look good. The suite passes locally (67/67), and the new test fails on main as it should.

There's one blocker left before I can approve. Please review and fix:

ctaSection still renders an empty heading that names the section (app/components/page/PageSections.vue:49, app/components/fridge/FridgeClosingCta.vue)

heading is optional in the ctaSection schema, but PageSections still passes section.heading || ''. FridgeClosingCta always renders the <h2> and always binds aria-labelledby, so a CTA block with no heading renders:

<section class="closing" aria-labelledby="v-1"><h2 id="v-1" class="display"></h2><p class="note"></p>…</section>

This is the same empty-heading problem (an unnamed region) that you fixed for the card grid. Please use the same pattern here:

defineProps<{ heading?: string; note?: string; cta: Cta; secondaryCta?: Cta }>()
<section class="closing" :aria-labelledby="heading ? headingId : undefined">
  <h2 v-if="heading" :id="headingId" class="display">{{ heading }}</h2>
  <p v-if="note" class="note">{{ note }}</p>

Then pass section.heading and section.note straight through in PageSections, and add a "CTA with no heading" case to tests/sectionHeadingIds.test.ts next to the prose and grid cases.

Dropping #3 from my earlier review (<br></br> / <hr></hr> in ActivityGrid.vue). I had the impact wrong. Vue's template compiler drops the stray closing tags, so the output is a plain <br><hr><br> and nothing renders twice. It's unrelated to #80, so it isn't a blocker here and can be cleaned up separately.

@philorien philorien left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jay23606, 0148748 fixes the CTA empty-heading case exactly as asked, and the new test cases cover it. All 67 tests pass locally, both on the branch and merged onto current main. Approving.

jay23606 and others added 3 commits October 15, 2026 21:46
ContactSection and FridgeClosingCta hard-coded their heading and error ids,
so two on one page collided. Derive them with useId. A prose or grid section
without a heading is no longer labelled by a missing id, and the grid no
longer renders an empty h2.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
ctaSection has an optional heading, but FridgeClosingCta always rendered an
empty h2 and bound aria-labelledby to it, which named the region with nothing.
Make heading and note optional, render each only when present, and pass the
section values straight through. Add a CTA case to the heading-ID test.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
@philorien
philorien force-pushed the pr-scout/80-a11y-duplicate-heading-ids-on-projects-m branch from 0148748 to caf8791 Compare October 16, 2026 01:46
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dcma-site Ready Ready Preview Oct 16, 2026 1:50am UTC

@philorien
philorien merged commit cb7a74a into philorien:main Oct 16, 2026
3 checks passed
@jay23606
jay23606 deleted the pr-scout/80-a11y-duplicate-heading-ids-on-projects-m branch October 16, 2026 17:24

This branch was successfully deployed

1 active deployment
Preview — caf8791f Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[a11y] Duplicate heading ids on /projects mislabel sections

2 participants