diff --git a/data/accessibility.json b/data/accessibility.json new file mode 100644 index 00000000..0235733c --- /dev/null +++ b/data/accessibility.json @@ -0,0 +1,54 @@ +{ + "overallScore": 86, + "wcagLevelTarget": "AA", + "auditChecks": [ + { + "name": "Keyboard navigation", + "status": "pass", + "description": "Primary navigation, forms, and dialogs are fully operable using keyboard controls.", + "recommendation": "Maintain keyboard testing in release QA to prevent focus order regressions." + }, + { + "name": "Screen reader support", + "status": "warning", + "description": "Most semantic landmarks are announced correctly, but some dynamic announcements are delayed.", + "recommendation": "Add aria-live regions for async success and error state messaging." + }, + { + "name": "Color contrast", + "status": "fail", + "description": "Several secondary text tokens and disabled states fall below WCAG AA contrast thresholds.", + "recommendation": "Raise contrast tokens to at least 4.5:1 for normal text and 3:1 for UI indicators." + }, + { + "name": "Alt text", + "status": "pass", + "description": "Meaningful imagery includes descriptive alternatives and decorative assets are hidden from assistive tech.", + "recommendation": "Continue enforcing alt text checks in content publishing workflows." + }, + { + "name": "Focus indicators", + "status": "warning", + "description": "Focus rings are visible in most components but can be hard to detect on gradient backgrounds.", + "recommendation": "Increase focus ring thickness and contrast on dark and gradient surfaces." + }, + { + "name": "Heading structure", + "status": "pass", + "description": "Pages consistently use hierarchical heading order to support assistive navigation.", + "recommendation": "Keep heading lint checks enabled in CI for template changes." + }, + { + "name": "Form labels", + "status": "pass", + "description": "Inputs are programmatically associated with labels and include accessible error text.", + "recommendation": "Add label association tests for new dynamic form components." + }, + { + "name": "ARIA attributes", + "status": "fail", + "description": "A subset of interactive widgets has incomplete aria-expanded and aria-controls state sync.", + "recommendation": "Audit custom interactive controls and ensure ARIA state is updated on every interaction." + } + ] +} diff --git a/src/app/accessibility/accessibility-page-client.tsx b/src/app/accessibility/accessibility-page-client.tsx new file mode 100644 index 00000000..0db5210f --- /dev/null +++ b/src/app/accessibility/accessibility-page-client.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { AlertTriangle, CheckCircle2, CircleAlert, Loader2, ShieldAlert, ShieldCheck } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import type { + AccessibilityApiResponse, + AccessibilityAuditCheck, + AccessibilityCheckStatus, + AccessibilityComplianceStatus, +} from "@/lib/accessibility"; + +function statusStyles(status: AccessibilityCheckStatus) { + switch (status) { + case "pass": + return "bg-emerald-500/15 border-emerald-400/30 text-emerald-300"; + case "warning": + return "bg-amber-500/15 border-amber-400/30 text-amber-300"; + default: + return "bg-red-500/15 border-red-400/30 text-red-300"; + } +} + +function statusIcon(status: AccessibilityCheckStatus) { + switch (status) { + case "pass": + return ; + case "warning": + return ; + default: + return ; + } +} + +function complianceStyles(status: AccessibilityComplianceStatus) { + switch (status) { + case "compliant": + return "bg-emerald-500/15 border-emerald-400/30 text-emerald-300"; + case "partial": + return "bg-amber-500/15 border-amber-400/30 text-amber-300"; + default: + return "bg-red-500/15 border-red-400/30 text-red-300"; + } +} + +function scoreStyles(score: number) { + if (score >= 90) return "text-emerald-300"; + if (score >= 75) return "text-amber-300"; + return "text-red-300"; +} + +export function AccessibilityPageClient() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + + const load = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch("/api/accessibility", { cache: "no-store" }); + const payload = (await response.json()) as AccessibilityApiResponse & { error?: string }; + + if (!response.ok) { + throw new Error(payload.error || "Failed to load accessibility audit data."); + } + + if (mounted) { + setData(payload); + } + } catch (loadError) { + if (mounted) { + setError(loadError instanceof Error ? loadError.message : "Unexpected error"); + } + } finally { + if (mounted) { + setLoading(false); + } + } + }; + + void load(); + + return () => { + mounted = false; + }; + }, []); + + const failedChecks = useMemo(() => { + if (!data) return []; + return data.auditChecks.filter((check) => check.status === "fail"); + }, [data]); + + if (loading) { + return ( + + +

Loading accessibility audit results...

+
+ ); + } + + if (error || !data) { + return ( + + +

{error || "Unable to load accessibility audit results."}

+ +
+ ); + } + + return ( +
+ +
+
+

Accessibility Compliance Snapshot

+

Persistent audit results from the latest accessibility review cycle.

+
+ +
+

{data.overallScore}

+

Score out of 100

+
+
+ +
+ + {data.complianceStatus === "compliant" ? : } + {data.complianceStatus} + + WCAG Target: {data.wcagLevelTarget} + {data.summary.pass} pass + {data.summary.warning} warning + {data.summary.fail} fail +
+
+ + +

Audit Checks

+
+ {data.auditChecks.map((check: AccessibilityAuditCheck) => ( +
+
+

{check.name}

+ + {statusIcon(check.status)} + {check.status} + +
+

{check.description}

+
+ ))} +
+
+ + +

Priority Recommendations

+ {failedChecks.length === 0 ? ( +
+ No failing checks. Continue monitoring warnings and maintain regression testing. +
+ ) : ( +
+ {failedChecks.map((check) => ( +
+

{check.name}

+

{check.recommendation}

+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/app/accessibility/page.tsx b/src/app/accessibility/page.tsx index 3f5e91ac..d82b18a0 100644 --- a/src/app/accessibility/page.tsx +++ b/src/app/accessibility/page.tsx @@ -1,23 +1,15 @@ -import Link from "next/link"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { - Keyboard, - Volume2, - Eye, - Focus, - ImageIcon, - Code2, - AlertCircle, - ExternalLink, -} from "lucide-react"; +/* eslint-disable react/no-unescaped-entities */ +import type { Metadata } from "next"; +import { AccessibilityPageClient } from "./accessibility-page-client"; -export const metadata = { +export const metadata: Metadata = { title: "Accessibility — forAgents.dev", - description: "Our commitment to making forAgents.dev accessible to everyone. WCAG 2.1 AA compliance standards and practices.", + description: + "Live accessibility compliance status for forAgents.dev, including WCAG target level, audit checks, and remediation recommendations.", openGraph: { title: "Accessibility — forAgents.dev", - description: "Our commitment to making forAgents.dev accessible to everyone. WCAG 2.1 AA compliance standards and practices.", + description: + "Live accessibility compliance status for forAgents.dev, including WCAG target level, audit checks, and remediation recommendations.", url: "https://foragents.dev/accessibility", siteName: "forAgents.dev", type: "website", @@ -25,331 +17,18 @@ export const metadata = { }; export default function AccessibilityPage() { - const practices = [ - { - icon: Keyboard, - title: "Keyboard Navigation", - description: - "Full keyboard navigation support with visible focus indicators. Navigate the entire site using Tab, Enter, and arrow keys without a mouse.", - }, - { - icon: Volume2, - title: "Screen Reader Support", - description: - "Semantic HTML and ARIA labels ensure compatibility with screen readers like NVDA, JAWS, and VoiceOver for blind and low-vision users.", - }, - { - icon: Eye, - title: "Color Contrast", - description: - "Text and interactive elements meet WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text) for readability.", - }, - { - icon: Focus, - title: "Focus Management", - description: - "Clear focus states on all interactive elements. Focus is managed predictably during navigation and modal interactions.", - }, - { - icon: ImageIcon, - title: "Alt Text", - description: - "All meaningful images include descriptive alt text. Decorative images are marked appropriately to avoid screen reader clutter.", - }, - { - icon: Code2, - title: "Semantic HTML", - description: - "Proper HTML5 semantic elements (nav, main, article, section) provide structure and improve navigation for assistive technologies.", - }, - ]; - - const resources = [ - { - title: "WCAG 2.1 Guidelines", - url: "https://www.w3.org/WAI/WCAG21/quickref/", - description: "Official Web Content Accessibility Guidelines", - }, - { - title: "WebAIM", - url: "https://webaim.org/", - description: "Resources and articles on web accessibility", - }, - { - title: "axe DevTools", - url: "https://www.deque.com/axe/devtools/", - description: "Browser extension for accessibility testing", - }, - { - title: "WAVE Tool", - url: "https://wave.webaim.org/", - description: "Web accessibility evaluation tool", - }, - ]; - return ( -
- - {/* Hero Section */} -
- {/* Subtle aurora background */} -
-
-
-
- -
-

- Accessibility -

-

- Building for everyone, including AI agents -

-
-
- - - - {/* Our Commitment */} -
-
-
-
-
- -

Our Commitment

-
-
-

- forAgents.dev is - committed to ensuring digital accessibility for people with - disabilities. We are continually improving the user experience - for everyone and applying relevant accessibility standards. -

-

- Our goal is to meet or exceed{" "} - - WCAG 2.1 Level AA - {" "} - conformance. This includes making our content perceivable, - operable, understandable, and robust for all users — including - those using assistive technologies and autonomous AI agents. -

-

- Because we're an agent-native platform, we design with both - human and machine accessibility in mind. Clean semantic markup, - predictable structure, and machine-readable endpoints benefit - everyone. -

-
-
-
-
- - - - {/* What We Do */} -
+
+
-

What We Do

-

- Our accessibility practices and standards -

-
- -
- {practices.map((practice, index) => { - const Icon = practice.icon; - return ( -
-
-
-
-
- -
-

- {practice.title} -

-
-

- {practice.description} -

-
-
- ); - })} -
-
- - - - {/* Known Limitations */} -
-
-
-
-
- -

Known Limitations

-
-
-

- We're committed to transparency about where we can improve. - Current known accessibility gaps include: -

-
    -
  • - Some dynamic content updates may not announce properly to - screen readers -
  • -
  • - Third-party embedded content (GitHub cards, social embeds) may - not meet our accessibility standards -
  • -
  • - Complex data visualizations and charts need improved - descriptive alternatives -
  • -
  • - Some modal dialogs may trap focus unpredictably in edge cases -
  • -
-

- We're actively working to address these issues. If you - encounter barriers not listed here, please let us know. -

-
-
-
-
- - - - {/* Report an Issue */} -
-
-
-
- -
-
- 🐛 -

Report an Issue

-
- -

- If you experience any accessibility barriers on forAgents.dev, - please let us know. Your feedback helps us improve for everyone. -

- -
-
- - - Describe the issue and which page or feature you encountered - it on - -
-
- - - Include your assistive technology (screen reader, browser - extension, etc.) - -
-
- - - Let us know your browser and operating system - -
-
- - -
-
-
- - - - {/* Resources */} -
-
-

📚 Resources

-

- Learn more about web accessibility -

-
- -
- {resources.map((resource, index) => ( - - - - {resource.title} - - - - -

- {resource.description} -

- - {resource.url} - -
-
- ))} -
-
- - - - {/* Feedback Welcome */} -
-
-

- Accessibility is an ongoing effort. We welcome your feedback and - suggestions. -

-

- Last updated: February 2026 +

Accessibility

+

+ We're publishing real accessibility audit data sourced from persistent records through the /api/accessibility endpoint.

-
+ +
); } diff --git a/src/app/api/accessibility/route.ts b/src/app/api/accessibility/route.ts new file mode 100644 index 00000000..4fb3197e --- /dev/null +++ b/src/app/api/accessibility/route.ts @@ -0,0 +1,103 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { NextResponse } from "next/server"; +import type { + AccessibilityApiResponse, + AccessibilityAuditCheck, + AccessibilityAuditData, + AccessibilityCheckStatus, + AccessibilityComplianceStatus, +} from "@/lib/accessibility"; + +export const runtime = "nodejs"; + +const ACCESSIBILITY_DATA_PATH = path.join(process.cwd(), "data", "accessibility.json"); +const VALID_STATUSES: AccessibilityCheckStatus[] = ["pass", "warning", "fail"]; + +function isValidCheck(check: AccessibilityAuditCheck): boolean { + return ( + typeof check.name === "string" && + VALID_STATUSES.includes(check.status) && + typeof check.description === "string" && + typeof check.recommendation === "string" + ); +} + +function isValidAuditData(data: AccessibilityAuditData): boolean { + return ( + typeof data.overallScore === "number" && + data.overallScore >= 0 && + data.overallScore <= 100 && + ["AA", "AAA"].includes(data.wcagLevelTarget) && + Array.isArray(data.auditChecks) && + data.auditChecks.length === 8 && + data.auditChecks.every(isValidCheck) + ); +} + +function getComplianceStatus(data: AccessibilityAuditData): AccessibilityComplianceStatus { + const failCount = data.auditChecks.filter((check) => check.status === "fail").length; + + if (data.overallScore >= 90 && failCount === 0) { + return "compliant"; + } + + if (data.overallScore >= 75 && failCount <= 2) { + return "partial"; + } + + return "non-compliant"; +} + +function buildResponse(data: AccessibilityAuditData): AccessibilityApiResponse { + const pass = data.auditChecks.filter((check) => check.status === "pass").length; + const warning = data.auditChecks.filter((check) => check.status === "warning").length; + const fail = data.auditChecks.filter((check) => check.status === "fail").length; + + return { + ...data, + complianceStatus: getComplianceStatus(data), + summary: { + pass, + warning, + fail, + totalChecks: data.auditChecks.length, + }, + }; +} + +async function readAccessibilityData(): Promise { + try { + const raw = await fs.readFile(ACCESSIBILITY_DATA_PATH, "utf-8"); + const parsed = JSON.parse(raw) as AccessibilityAuditData; + + if (!isValidAuditData(parsed)) { + return null; + } + + return parsed; + } catch { + return null; + } +} + +export async function GET() { + const data = await readAccessibilityData(); + + if (!data) { + return NextResponse.json( + { + error: "Accessibility audit data unavailable", + }, + { + status: 500, + } + ); + } + + return NextResponse.json(buildResponse(data), { + headers: { + "Cache-Control": "no-store", + }, + }); +} diff --git a/src/lib/accessibility.ts b/src/lib/accessibility.ts new file mode 100644 index 00000000..70991f63 --- /dev/null +++ b/src/lib/accessibility.ts @@ -0,0 +1,27 @@ +export type AccessibilityCheckStatus = "pass" | "fail" | "warning"; +export type WcagLevelTarget = "AA" | "AAA"; + +export interface AccessibilityAuditCheck { + name: string; + status: AccessibilityCheckStatus; + description: string; + recommendation: string; +} + +export interface AccessibilityAuditData { + overallScore: number; + wcagLevelTarget: WcagLevelTarget; + auditChecks: AccessibilityAuditCheck[]; +} + +export type AccessibilityComplianceStatus = "compliant" | "partial" | "non-compliant"; + +export interface AccessibilityApiResponse extends AccessibilityAuditData { + complianceStatus: AccessibilityComplianceStatus; + summary: { + pass: number; + warning: number; + fail: number; + totalChecks: number; + }; +}