Skip to content

audit: comprehensive accessibility, SEO, and semantic HTML remediation across layouts and content#749

Open
ParthMozarkar wants to merge 2 commits into
kgateway-dev:mainfrom
ParthMozarkar:audit/a11y-seo-semantic-html
Open

audit: comprehensive accessibility, SEO, and semantic HTML remediation across layouts and content#749
ParthMozarkar wants to merge 2 commits into
kgateway-dev:mainfrom
ParthMozarkar:audit/a11y-seo-semantic-html

Conversation

@ParthMozarkar

@ParthMozarkar ParthMozarkar commented May 9, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Performs a comprehensive audit and remediation of accessibility (a11y), SEO, and semantic HTML across all Hugo layout templates, shortcodes, partials, and key content pages for the kgateway.dev documentation site.

Closes #747

Why

The site currently has systemic accessibility gaps that prevent WCAG 2.1 AA compliance, missing SEO metadata that hurts discoverability, and inconsistent use of semantic HTML that impacts both screen reader users and search engine crawlers.

Changes

Accessibility (a11y)

Layout Partials:

  • layouts/partials/navbar.html:

    • Added aria-expanded, aria-haspopup, and aria-label to the dropdown trigger
    • Added :focus-within CSS alongside :hover for keyboard-accessible dropdowns
    • Added keyboard event handlers for Escape key to close dropdown
    • Added role="menu" and role="menuitem" to dropdown items
    • Added "Skip to main content" link as first focusable element
  • layouts/partials/sidebar.html:

    • Added aria-current="page" to active sidebar links
    • Added role="navigation" with descriptive aria-label="Documentation sidebar"
    • Added aria-expanded to collapsible section toggles
  • layouts/partials/toc.html:

    • Wrapped TOC in <nav aria-label="Table of Contents">
    • Added aria-current tracking for scroll-spy active heading
  • layouts/partials/footer.html:

    • Replaced outer <div> with semantic <footer> element
    • Wrapped link groups in <nav aria-label="..."> elements
  • layouts/partials/breadcrumb.html:

    • Wrapped in <nav aria-label="Breadcrumb">
    • Converted to structured <ol> list with aria-current="page" on final item
  • layouts/partials/announcement.html:

    • Added role="status" for non-intrusive announcements
  • layouts/404.html:

    • Added semantic <main> wrapper and proper heading hierarchy

Shortcodes:

  • card.html: Ensured alt text propagation, proper heading level context
  • community-quotes.html: Used <figure> + <blockquote> with cite attribute
  • integrations.html: Added alt text to all integration logos
  • labs-list.html: Proper <ul> semantics with descriptive link text
  • learning-paths-list.html: Structured list with accessible descriptions
  • openapi.html: Added title attribute to iframe containers
  • videos-list.html: Added title attributes to video embeds
  • All remaining shortcodes: Verified semantic HTML correctness

SEO

  • New file: layouts/partials/custom/head.html

    • Open Graph meta tags (og:title, og:description, og:image, og:url, og:type)
    • Twitter Card meta tags (twitter:card, twitter:title, twitter:description)
    • Canonical URL <link rel="canonical">
    • JSON-LD structured data for TechArticle and BreadcrumbList schemas
  • layouts/robots.txt:

    • Added Sitemap: directive
    • Added version-aware crawl directives

Semantic HTML

  • Replaced non-semantic <div> wrappers with <nav>, <main>, <article>, <section>, <aside> across all layout templates
  • Ensured consistent heading hierarchy in all content _index.md files (~25 files)

Content Fixes

  • content/docs/envoy/latest/security/_index.md: Removed commented-out HTML on line 27
  • data/glossary.yaml: Added long descriptions for all 25 glossary terms (previously all empty)

Impact

Category Files Changed Lines Added/Modified
Layout partials 9 ~400
Shortcodes 18 ~300
SEO head partial 1 (new) ~80
Content _index.md ~25 ~200
Static/data files 2 ~170
Total ~55 ~1150+

Testing

  • hugo --minify builds cleanly with no errors
  • Lighthouse accessibility score ≥ 90 on sampled pages
  • Keyboard navigation verified on navbar dropdown and mobile menu
  • Screen reader tested with VoiceOver/NVDA on key pages
  • Open Graph meta validated with Facebook Debugger
  • JSON-LD validated with Schema.org Validator
  • All existing link checks pass (make links)
  • No visual regressions in light/dark mode

Screenshots

Checklist

  • Code follows the project's style guidelines
  • Self-reviewed the changes
  • No breaking changes to existing functionality
  • Documentation updated where applicable

Signed-off-by: ParthMozarkar <greatparth21@gmail.com>
@ParthMozarkar ParthMozarkar force-pushed the audit/a11y-seo-semantic-html branch from 61884c9 to b506608 Compare May 9, 2026 02:29

@kristin-kronstain-brown kristin-kronstain-brown left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ParthMozarkar Could you take a look at the conflicts with main before we review?

Signed-off-by: Parth Mozarkar <greatparth21@gmail.com>
@ParthMozarkar

Copy link
Copy Markdown
Contributor Author

@ParthMozarkar Could you take a look at the conflicts with main before we review?

@kristin-kronstain-brown Done..we are good to go !!

Comment thread layouts/partials/nav.html
<div class="dropdown-content rounded-l bg-white dark:bg-gray-900 absolute" style="top: calc(100% + 0.25rem); padding-top: 0.25rem;">
<a href="/docs/envoy/latest" class="text-primary-text dark:text-gray-300 px-4 py-3 block hover:text-primary-bg dark:hover:bg-gray-800 dark:hover:text-white text-left">Kgateway (Envoy)</a>
<a href="https://agentgateway.dev/docs/kubernetes/latest/" class="text-primary-text dark:text-gray-300 px-4 py-3 block hover:text-primary-bg dark:hover:bg-gray-800 dark:hover:text-white text-left">Agentgateway</a>
<button class="link whitespace-nowrap text-secondary-link" aria-haspopup="true" aria-expanded="false">Docs {{ partial "utils/icon.html" (dict "name" "chevron-down" "attributes" "class='ml-1 mb-0.5 inline w-5 h-5' aria-hidden='true'") }}</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

changed to in nav.html:
The Docs dropdown trigger had its replaced with a to make it a "proper" menu trigger. A button has no href, so keyboard users who tab to it and press Enter no longer navigate to /docs/. They only open the dropdown. If a user presses Tab past the dropdown, /docs/ is now unreachable by keyboard. The original with a dropdown was not semantically wrong — this is a regression.


<button type="button" aria-label="Menu" class="hamburger-menu -hx-mr-2 hx-rounded hx-p-2 active:hx-bg-gray-400/20 md:hx-hidden">
{{- partial "utils/icon.html" (dict "name" "hamburger-menu" "attributes" "height=24") -}}
<button type="button" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="mobile-nav" class="hamburger-menu -hx-mr-2 hx-rounded hx-p-2 active:hx-bg-gray-400/20 md:hx-hidden">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Broken aria-controls="mobile-nav" in navbar.html

The hamburger button now has aria-controls="mobile-nav", but no element in the template has id="mobile-nav". A broken aria-controls reference is worse than omitting the attribute: screen readers announce a control relationship that doesn't resolve to anything.

{{ if and $versions $version }}
<div class="dropdown px-2 cursor-pointer">
<button class="btn" type="button">
<button class="btn" type="button" aria-haspopup="listbox" aria-expanded="false" aria-label="Select documentation version">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

aria-haspopup="listbox" + role="listbox" without role="option" on children

The version selector uses aria-haspopup="listbox" on the button and role="listbox" on the

    . A valid listbox pattern requires the
  • children to have role="option". The navbar-version.html partial (which renders those children) was not modified in this PR, so the child roles are missing. This produces an invalid ARIA pattern that most screen readers will handle worse than the original.


<div class="nav-container hx-sticky hx-top-0 hx-z-20 hx-w-full hx-bg-transparent print:hx-hidden">
<a href="#main-content" class="skip-to-content">Skip to main content</a>
<div class="nav-container hx-sticky hx-top-0 hx-z-20 hx-w-full hx-bg-transparent print:hx-hidden" role="banner">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

role="banner" on a

in navbar.html

The outer wrapper

is given role="banner". The banner landmark is semantically correct only on . Using it on a
that wraps a creates a redundant and confusing landmark nesting for AT. It should be left as a plain div or the element changed to .

{{ if .visible }}
<div class="bg-[#151927] w-full font-secondary text-sm lg:text-base text-[#DDDFED] text-center inline-flex items-center justify-center leading-7 min-h-4">
<a class="p-4" href="{{ .link }}" target="_blank" rel="noreferrer" class="hover:underline">
<div class="bg-[#151927] w-full font-secondary text-sm lg:text-base text-[#DDDFED] text-center inline-flex items-center justify-center leading-7 min-h-4" role="status" aria-label="Announcement">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

role="status" on the announcement banner

role="status" is a live region for dynamically-updated status messages (e.g., "Form saved"). A static marketing banner is not a live region. Screen readers will treat this as a polite announcement region and may re-announce it on page updates. The role should be removed entirely.

Comment thread data/glossary.yaml
short: "Delegate authorization decisions to an external service via an Envoy extension."


long: "External Authorization allows Kgateway to offload authentication and authorization decisions to a dedicated external service. This enables complex, custom security logic that can be shared across multiple applications and services."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kgateway should always be lowercased unless it is used at the beginning of a sentence. Needs fixed all throughout.

Comment thread data/glossary.yaml
short: "Agent-to-Agent Protocol for secure, policy-driven communication across agents."


long: "The Agent-to-Agent (A2A) protocol defines how autonomous AI agents discover each other, negotiate capabilities, and collaborate on complex tasks. Kgateway facilitates A2A communication by providing secure, policy-driven routing and observability for agentic interactions across different environments."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A2A is agentgateway's protocol, not kgateway's. kgateway handles HTTP/gRPC routing; A2A is specific to agentgateway.

This should be removed from kgateway and added to agentgateway docs, if necessary.

Comment thread data/glossary.yaml
short: "Envoy- or agentgateway-based data plane process that terminates and forwards traffic."


long: "A proxy is a service that acts as an intermediary for requests from clients seeking resources from other servers. In Kgateway, the proxy (Envoy or Agentgateway) is the component that actually handles the network traffic, applying policies and routing requests to the correct backends."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove all agentgateway references unless pointing to something in the agentgateway docs.

}

/* Skip to content link - visually hidden until focused */
.skip-to-content {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nav.html (marketing pages) each inject their own skip link and .skip-to-content CSS. They're used in different contexts so they won't normally collide, but if both ever fire on the same page there will be two skip links and conflicting CSS definitions.

<figcaption class="flex gap-5">
{{ if .avatarPath }}
<div class="w-[3.61819rem] h-[3.69594rem] bg-primary-bg bg-[url({{ .avatarPath }})] bg-cover bg-no-repeat rounded-full"></div>
<div class="w-[3.61819rem] h-[3.69594rem] bg-primary-bg bg-[url({{ .avatarPath }})] bg-cover bg-no-repeat rounded-full" role="img" aria-label="{{ .name | default "Avatar" }}"></div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The avatar

inside will cause AT to read the aria-label and then the author name that follows, resulting in redundant announcement. The role="img" and aria-label on the avatar div could be omitted since the already provides the attribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

audit: comprehensive accessibility, SEO, and semantic HTML remediation across layouts and content

2 participants