Direct answer: the 25 homepages carry 1,765 id attributes; 10 sites contain duplicates, with 48 duplicated names and 849 extra occurrences. The worst are E03 (233 extras), E22 (154), E06 (149), E14 (120) and E23 (105); E17 has eight duplicate names but only ten extras. The pages hold just four label for and ARIA references, all of which resolve (zero dangling). A post-render recheck (15 renderable sites) found 12 sites adding ids and E26 growing from 2 duplicate names / 17 extras to 4 names / 33 extras — a JavaScript clone copies ids; ten sites failed to render and are excluded. On the reference side, the 25 pages hold four page references, all resolving, and 183 inline-script id lookups include one duplicate hit (E11 count-number).
The analysis strips <script>, <style> and comments first, so id="…" strings inside JavaScript templates do not inflate the counts; only static HTML is counted.
Method: strip scripts, then count static ids
On 2026-11-09 between 13:41 and 13:42 we requested the HTML of the same 25 cached-200 homepages, removed <script>, <style> and HTML comments, then extracted id="…" and counted duplicate names. We also collected ids referenced by label for and aria-labelledby/describedby/controls/owns/activedescendant and checked whether they exist in the document.
| Measure | Rule | Boundary |
|---|---|---|
| id attribute | Occurrences of id="…" in static HTML | Strings in scripts, styles and comments excluded |
| Duplicate name | A name appearing more than once | Case compared as written |
| Extra occurrences | Sum of (count − 1) for each duplicate name | Not the number of duplicate names |
| Reference | label for plus aria idrefs | Existence only, order not checked |
| Dangling reference | Target id absent from the document | A duplicated target still counts as present |
Result: ten sites repeat, a few heavily
| Measure | Count | Sites |
|---|---|---|
| id attributes | 1,765 | 24 |
| Duplicate names | 48 | 10 |
| Extra occurrences | 849 | 10 |
| label / ARIA references | 4 | 1 |
| Dangling references | 0 | 0 |
| Site | ids | Duplicate names | Extra occurrences |
|---|---|---|---|
| E03 | 329 | 7 | 233 |
| E22 | 186 | 6 | 154 |
| E06 | 172 | 5 | 149 |
| E14 | 177 | 4 | 120 |
| E23 | 295 | 7 | 105 |
| E09 | 87 | 2 | 48 |
| E26 | 39 | 2 | 17 |
| E17 | 74 | 8 | 10 |
| E11 | 14 | 1 | 7 |
| E24 | 90 | 6 | 6 |
Two shapes appear: E03, E22, E06, E14 and E23 repeat one id hundreds of times, typical of a carousel or card template that keeps a fixed id inside a loop; E17 and E24 repeat several different ids a few times, more like a component rendered twice on the page. The first shape is more likely to make getElementById and fragment links hit the wrong element.
Counting again after rendering: one site adds duplicates
On 2026-11-09 between 14:41 and 14:45 we captured the rendered DOM of the 25 sites with headless Chrome (--dump-dom, 15-second virtual time, a separate profile per site) and applied the same id rules. Fifteen sites rendered; ten were excluded for 403, challenge pages or timeouts.
| Measure (15 renderable sites) | Static | Rendered |
|---|---|---|
| Total ids | 343 | 1,162 |
| Duplicate names | 11 | 13 |
| Extra occurrences | 34 | 50 |
| Sites with duplicates | 3 | 3 |
| E26 extra occurrences | 17 | 33 |
Twelve sites change their id count after rendering: E02 goes from 6 to 644 (all unique), E25 7→133, E26 39→57 and E18 1→17; E28 drops from 1 to 0, meaning a script rewrote the DOM. E26 is the only site that gains duplicates: duplicate names 2→4 and extra occurrences 17→33, direct evidence that a JavaScript clone copies ids; E11 and E17 keep the same duplicates before and after, so theirs come from the static template. Sites with the heaviest static duplicates — E03, E06, E14, E22, E23 and E24 — did not render this time, so they cannot be compared.
Reference side: 4 page references and 183 script id lookups, one duplicate hit
Whether a duplicate id matters depends on who references it. On 2026-11-09 between 17:42 and 17:43 we fetched the same 25 homepages as static HTML again and parsed label for, ARIA idrefs and real href="#id" anchors, then scanned inline scripts for literal getElementById, querySelector('#id') and jQuery $('#id') calls.
| Reference kind | Count | Points at a duplicate | Dangling |
|---|---|---|---|
| label for / ARIA idrefs | 4 | 0 | 0 |
| href="#id" anchor | 0 | 0 | 0 |
| Inline-script id lookup | 183 (18 sites) | 1 | — |
The only hit is E11: the page has eight id="count-number" elements, so jQuery's #count-number reaches only the first p.timer.count-title and the other seven never update; the remaining 182 lookups point at unique names. Of the 48 duplicated names, just this one is referenced by a static script, and there are no page anchors — the risk is latent rather than widespread here.
Rule fix: onclick="this.href='#x'" is not a static anchor, and both the probe and the id checker now exclude it; E04 generates skip links in script and targets <a name>, so those are not counted as id references.
What this means for acceptance
- Add id uniqueness to HTML validation. Ten of 25 sites fail it, and most validators catch it with one rule.
- Rewrite ids when cloning a component. One name on E03 repeats over two hundred times; append an index in template loops or switch to a class.
- Use classes for styling hooks. Ids written only to attach styles are the ones that collide when copied.
- Check anchors and ARIA targets. Duplicate ids point fragments and aria references at the first element; references are few here, but the risk remains.
- Repeat the check after rendering. In the 15-site recheck, E26 moved from 2 to 4 duplicate names; static uniqueness does not imply runtime uniqueness, so rerun after cloning components. The id uniqueness checker reruns the rules locally in a browser; see also the website development service.
Limits
- Static HTML only: scripts, styles and comments are stripped; runtime ids are not counted.
- Regex approximation: no DOM is built; case is compared as written.
- Reference scope: pages cover label for, ARIA idrefs and static
href="#id"anchors; scripts cover literal inline getElementById/querySelector/jQuery calls, not external bundles or concatenated ids. - Existence rule: a duplicated id still counts as present, so pointing at the wrong element is not flagged.
- Script hit: it means the selector reaches the first same-name element only; the page script is not executed and the intended element is not judged.
- Render boundary: 15 of 25 sites rendered; ten were excluded for 403, challenge pages or timeouts, and 15 seconds of virtual time cannot see later asynchronous insertion.
- 25-site pilot: the same equipment-company sample; no industry claim.
Data and reproduction
Anonymised data: equipment-homepage-id-uniqueness-2026.csv (25 rows × 8 columns: ids, duplicate names, extra occurrences, references/dangling and time). The repository keeps docs/research/2026-11-homepage-id-uniqueness.csv.
Rendered recheck: equipment-homepage-id-runtime-2026.csv (25 rows × 14 columns: render status, static and rendered ids, duplicates, extra occurrences and time). The repository keeps docs/research/2026-11-homepage-id-runtime.csv.
Reference recheck: equipment-homepage-id-references-2026.csv (25 rows × 16 columns: page references, script id lookups, duplicate hits and time). The repository keeps docs/research/2026-11-homepage-id-references.csv.
Parser: scripts/id_uniqueness_probe.py; unit tests: scripts/test_content_id_uniqueness_probe.py (8, offline); reference and script scans are in scripts/test_content_id_reference_probe.py (13 tests). Run:
python -m unittest scripts.test_content_id_uniqueness_probe
Related: the homepage template-hygiene audit, the homepage script-hygiene audit, and homepage semantics, keyboard access and ARIA.
Sources and definitions
- Sample: the 25 study-4 homepages cached as 200.
- Fetch: 2026-11-09 13:41–13:42 Asia/Shanghai, serial, 0.5 s apart, 15-second timeout.
- Preprocessing: scripts, styles and comments removed before extracting ids.
- References: label for and aria idrefs; existence only, order not checked.
Frequently asked questions
How serious are duplicate ids?
They are an HTML validation error and make getElementById, fragment links and ARIA targets resolve to the first match only; cloned widgets hit this most often.
Is 1,765 ids a lot?
The count is not the issue; uniqueness is. Component anchors, forms and SVG all use ids, and the problems concentrate on a few sites with repeated names.
Does zero dangling references mean it is fine?
No. Only four label/ARIA references exist here, and duplicate ids can point a reference at the wrong element — “exists” is not “correct”.
Why are script-generated ids excluded?
The analysis strips <script> blocks first and counts static HTML; ids created at runtime need a post-render check.
How should it be fixed?
Prefix component ids or add an index, and rewrite ids when cloning a template; use classes or data attributes when the id is only a styling hook.
Can the data be re-checked?
Yes. The public CSV has 25 rows by 8 columns, and scripts/id_uniqueness_probe.py has 8 offline unit tests.
Do duplicates grow after rendering?
Sometimes, but only one site here. Among the 15 renderable sites, E26 moves from 2 to 4 duplicate names and from 17 to 33 extra occurrences; the rest stay flat or add only unique ids.
Do duplicate ids break scripts immediately?
Not necessarily. Of 183 inline-script id lookups across the 25 sites, one hits a duplicated name (E11 count-number, eight copies); a duplicate id is a latent risk waiting for a reference, not an automatic failure. External bundles, dynamic id strings and runtime insertion are out of scope.