fix(seo): resolve Ahrefs audit findings — orphan pages, double-slash URLs, indexing, alt text#531
Conversation
Delete 5 orphan Edge pages (no incoming links; live replacements exist): provision-customer, provision-user (→ provision-customers-and-users), provision-dashboard (→ dashboards), the edge-proxy parent Tabs page (→ edge-proxy/debian; children kept), and the empty pe/reference/configuration-reference stub (→ /docs/pe/reference/). Removes CE + PE stubs + the now-unused shared includes, adds the /user-guide/ + pe/ redirects. Remove the unlinked /community/ marketing page: delete the page, drop it from MARKETING_ALLOWLIST + the OG marketing-meta prefixes, and redirect /community/ → /. public/_redirects + public/redirects.json regenerated via pnpm generate:redirects.
An item whose itemType has no public category (e.g. DASHBOARD, hidden for the first release) produced /iot-hub//<slug>/. Guard the card-bind href to '#' and skip unknown types in the single-category grid, matching the existing groupResults/getListingHref pattern.
Remove the noIndex prop — the page is a full paginated IoT Hub catalog listing, a legit index page, not a query-driven search-results page.
CategoryTile emitted alt="" for the meaningful category image; use the category label.
There was a problem hiding this comment.
Review summary
Reviewed 23 changed files in fix(seo): resolve Ahrefs audit findings — orphan pages, double-slash URLs, indexing, alt text. Left 4 comments inline.
The cleanup is well-targeted — I verified every redirect target resolves to a real page (no redirect→404 chains), there are no dangling internal links to the deleted pages, and the double-slash guard reasoning is sound. One correctness issue stands out: the PE-variant redirects point at the wrong source URLs, so the canonical PE pages you deleted now 404 (inline on redirects.ts). I also re-checked the changes against the SEO audit — most in-scope items are done, but the alt-text cleanup is only partially addressed; see Additional findings.
Additional findings
These are about existing code outside the PR's diff — spotted while cross-checking the audit's missing-alt-text findings (ignoring the oversized-image item as requested).
- src/components/IotHub/CreatorHero.astro:33 — the creator avatar's
altshould be the creator name; it's stillalt="". The<img>sits inside anaria-hidden="true"wrapper, so screen-reader behavior is already correct (the name is in the adjacent<h1>) — but the Ahrefs "missing alt text" crawler flags emptyaltregardless, so creator pages likely still appear in the audit. Not touched by this PR. - src/components/IotHub/CategoryHero.astro:75 — the category hero-cluster image is still
alt=""; the audit flagged it as "meaningful". It's arguably decorative (illustration beside a heading), but it's unaddressed. - src/components/IotHub/IotHubNoResults.astro:29 and IotHubFetchError.astro:26 — still
alt="". These are decorative, so leaving them empty is defensible — just noting the audit may still count them. - src/pages/iot-hub/search/[...page].astro:81 — a more catalog-style
titlethan plain "Search" was suggested; it's stillSearch | IoT Hub | ThingsBoard. Optional, and thenoIndexremoval (the core of that item) is done.
Net: the orphan-page cleanup, double-slash guard, search indexing, and /community/ removal are all addressed in this repo (the orphan cleanup with the redirect caveat above); the alt-text pass only covers the category tiles (via CategoryTile.astro), leaving the creator avatar and hero-cluster images untouched. The device-library JSON redirect, oversized image, and CMS-authored alt text are correctly out of scope (iot-hub CMS).
This review was auto-generated. Findings may contain errors — please verify before applying changes.
| { oldPath: 'pe/edge/config/subscribe-to-attribute', target: '/docs/edge/pe/user-guide/attribute-sync/' }, | ||
| // Orphan Edge user-guide pages removed — redirect to their live replacements. | ||
| { oldPath: 'edge/user-guide/provision-customer', target: '/docs/edge/user-guide/provision-customers-and-users/' }, | ||
| { oldPath: 'pe/edge/user-guide/provision-customer', target: '/docs/edge/pe/user-guide/provision-customers-and-users/' }, |
There was a problem hiding this comment.
These four pe/edge/user-guide/... entries redirect the legacy URL scheme (/docs/pe/edge/...), but the pages you deleted were live at the canonical PE URLs — /docs/edge/pe/user-guide/{provision-customer,provision-user,provision-dashboard,edge-proxy}/ (that's where the deleted edge/pe/... stubs rendered, and where the pe/edge → edge/pe catch-all pointed search engines). After this PR nothing redirects those canonical URLs, so they now 404 — which reintroduces the exact orphan/404 problem this PR fixes, for the PE half of each deleted page.
The CE variants above are fine because a CE page's canonical URL is the edge/user-guide/... oldPath. For PE you'd want the canonical source too, e.g.:
{ oldPath: 'edge/pe/user-guide/provision-customer', target: '/docs/edge/pe/user-guide/provision-customers-and-users/' },
{ oldPath: 'edge/pe/user-guide/provision-user', target: '/docs/edge/pe/user-guide/provision-customers-and-users/' },
{ oldPath: 'edge/pe/user-guide/provision-dashboard', target: '/docs/edge/pe/user-guide/dashboards/' },
{ oldPath: 'edge/pe/user-guide/edge-proxy', target: '/docs/edge/pe/user-guide/edge-proxy/debian/' },Keep the existing pe/edge/... entries as well so the legacy URL doesn't chain into a 404. (I confirmed all the targets resolve to real pages — this is purely about the missing canonical sources.)
| const categorySlug = cat?.slug ?? ''; | ||
| // Unknown item type has no public category — skip rather than emit | ||
| // cards with a `/iot-hub//slug/` href (matches groupResults). | ||
| if (!cat) return; |
There was a problem hiding this comment.
The same "itemType has no public category" condition is now handled three ways across these files: getListingHref returns '#', bindListingCard returns '#', and here renderResults early-returns and drops the whole grid. For this single-category branch the early return is actually the right call — every item shares the page's itemType, so if that type has no category there's genuinely nothing renderable. Just flagging that one condition with three different fallbacks is easy to trip over later; a shared helper for the cat-lookup + skip/'#' decision would make it one choice instead of three. Not blocking.
| // Root href. An empty categorySlug (item type with no public category, e.g. | ||
| // a type the site doesn't surface) would yield `/iot-hub//slug/` — guard it | ||
| // to '#' instead, matching getListingHref. | ||
| root.setAttribute('href', categorySlug ? `/iot-hub/${categorySlug}/${item.slug}/` : '#'); |
There was a problem hiding this comment.
Heads-up that bindListingCard has no runtime caller — the only reference is a commented-out usage example in ListingCardTemplate.astro. So this new empty-slug guard hardens a path nothing currently exercises, and the comment asserting parity with getListingHref isn't exercised either. Fine to keep for symmetry, but worth confirming whether the binder is still meant to be wired up (or removed) rather than left as dead code.
| </span> | ||
| <div class="iot-hub-tile__img"> | ||
| <Image src={image} alt="" widths={[400, 600, 800]} sizes="(max-width: 750px) 100vw, 400px" /> | ||
| <Image src={image} alt={label} widths={[400, 600, 800]} sizes="(max-width: 750px) 100vw, 400px" /> |
There was a problem hiding this comment.
Minor a11y note (not blocking): the tile already renders {label} as visible text inside the same <a>, so alt={label} makes a screen reader announce the category name twice per link — its sibling CategoryHero.astro keeps alt="" for exactly this decorative-image-next-to-visible-text case. That said, since the whole point here is passing the Ahrefs "missing alt" audit (which flags empty alt), alt={label} is a reasonable tradeoff and I wouldn't revert it to "" without confirming the audit tolerates empty alt. Just noting the redundant SR output.
Orphan-page redirects were added only for the legacy pe/edge/ URL scheme, so the canonical edge/pe/ URLs of the deleted PE stubs 404'd. Add canonical sources (keep legacy aliases for one-hop resolution). Add alt text to the category hero and creator avatar images (creator avatar now exposed to the a11y tree as 'Photo of <name>'), and make the category-tile alt descriptive to avoid a duplicate screen-reader announcement.
…L byte - Add getCategoryForItemType() as the single itemType->category lookup (Map-backed, O(1)); route getListingHref and dynamic-search grouping/rendering through it and drop the duplicate module-local map. - Set the IoT Hub search page title to 'IoT Hub Catalog | ThingsBoard' so the now-indexable page reads as a catalog. - Remove a stray NUL byte in iot-hub-dynamic-search.ts (a typo for a space in an internal dedup key) that made the file read as binary and hid it from grep-based tooling. - Document bindListingCard's real runtime caller.
…s/; SEO alt text + search noindex (#533) * fix(iot-hub): relocate device-library resources under /iot-hub/devices/ Move public/device-library/resources/ -> public/iot-hub/devices/resources/ so the existing device-library -> IoT Hub splat redirects resolve to real files. Both /docs/devices-library/resources/* and /device-library/resources/* already 301 into /iot-hub/devices/:splat; the assets now live at that target, fixing broken links (e.g. the lrs10701 air-quality dashboard JSON referenced from external sites we cannot edit). No project code references these paths. * fix(seo): alt text on remaining IoT Hub images; keep search page noindexed Follow-up to the Ahrefs audit (#531): - Give the last four empty-alt IoT Hub images meaningful alt text: the no-results and fetch-error illustrations (aria-hidden kept so screen readers aren't given a redundant announcement), the detail creator avatar, and the listing-link thumbnail. - Revert /iot-hub/search/ to its original title and restore noIndex. Removing it wholesale also un-noindexed the paginated variants (/iot-hub/search/2/ ...), which self-canonicalize and would be indexed as near-duplicates; keep the page out of the index for now.
Resolves the in-repo findings from the post-release Ahrefs SEO audit of the docs site.
Changes
1. Orphan Edge pages → redirects
Removed stale Edge user-guide pages that nothing linked to (several had been renamed) and added redirects from the old URLs to their live replacements. The one reference page with no direct successor redirects to the PE reference landing page.
2. Double-slash URL guard (
/iot-hub//<slug>/)IoT Hub links are composed from
/iot-hub/+ category slug + item slug. When the backend returns an item whose type has no docs-site category (e.g. theDASHBOARDtype, live server-side but hidden on the site), the category slot was empty and produced a//URL. Both link builders (server-rendered cards and live search results) now skip such items instead of emitting a malformed href — matching the existinggetListingHref → '#'/groupResults → continuepatterns.3. Make
/iot-hub/search/indexableRemoved the
noIndexflag from the IoT Hub search page so search engines can list it.4. Alt text on IoT Hub category tiles
Category tile images now use the category name as alt text (was empty).
5. Remove retired
/community/pageDeleted the page, redirected
/community/→ homepage, and removed it from the OG-image allowlist / marketing-meta so the build no longer generates a card for a nonexistent page.Verification
astro check— 0 errors, 0 warnings, 0 hintseslint— cleanlint:linkcheck— full build (4466 pages), no link issuesOut of scope (IoT Hub CMS team)
Three audit findings live in the IoT Hub CMS, not this repo, and are not addressed here: device-library resource JSON redirect, an oversized image, and CMS-authored image alt text.