From a2eebfb7009bce63f93919dd5862c6f4e9a3ae98 Mon Sep 17 00:00:00 2001
From: AbuJulaybeeb
Date: Thu, 23 Jul 2026 23:52:44 +0100
Subject: [PATCH] a11y(docs): add proper heading structure
---
src/app/docs/Section.test.tsx | 58 +++++++++++++++++++++++++++++++++++
src/app/docs/Section.tsx | 27 ++++++++++++++++
src/app/docs/page.test.tsx | 18 +++++++++++
src/app/docs/page.tsx | 16 +++++-----
4 files changed, 110 insertions(+), 9 deletions(-)
create mode 100644 src/app/docs/Section.test.tsx
create mode 100644 src/app/docs/Section.tsx
diff --git a/src/app/docs/Section.test.tsx b/src/app/docs/Section.test.tsx
new file mode 100644
index 0000000..bcdc947
--- /dev/null
+++ b/src/app/docs/Section.test.tsx
@@ -0,0 +1,58 @@
+import { render } from "@testing-library/react";
+import { DocsSection } from "./Section";
+
+function getHeadingOutline(container: HTMLElement) {
+ const headings = Array.from(
+ container.querySelectorAll("h1, h2, h3, h4, h5, h6"),
+ );
+ return headings.map((h) => parseInt(h.tagName[1], 10));
+}
+
+function assertValidOutline(levels: number[]) {
+ if (levels.length === 0) return;
+ for (let i = 1; i < levels.length; i++) {
+ const prev = levels[i - 1];
+ const curr = levels[i];
+ expect(curr).toBeLessThanOrEqual(prev + 1);
+ }
+}
+
+describe("DocsSection heading outline", () => {
+ it("forms a valid outline without skipping levels (single section page)", () => {
+ const { container } = render(
+
+ Title
+
+ content
+
+ ,
+ );
+ const outline = getHeadingOutline(container);
+ assertValidOutline(outline);
+ expect(outline).toEqual([1, 2]);
+ });
+
+ it("forms a valid outline without skipping levels (deeply nested section)", () => {
+ const { container } = render(
+
+ Title
+
+
+
+
+
+
+ content
+
+
+
+
+
+
+ ,
+ );
+ const outline = getHeadingOutline(container);
+ assertValidOutline(outline);
+ expect(outline).toEqual([1, 2, 3, 4, 5, 6, 6]);
+ });
+});
diff --git a/src/app/docs/Section.tsx b/src/app/docs/Section.tsx
new file mode 100644
index 0000000..17c994e
--- /dev/null
+++ b/src/app/docs/Section.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import React from "react";
+
+// Context to track the current heading level
+export const LevelContext = React.createContext(2);
+
+interface DocsSectionProps {
+ heading: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export function DocsSection({ heading, children }: DocsSectionProps) {
+ const level = React.useContext(LevelContext);
+ const HeadingTag = `h${Math.min(level, 6)}` as keyof JSX.IntrinsicElements;
+
+ return (
+
+ {heading}
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/app/docs/page.test.tsx b/src/app/docs/page.test.tsx
index 50428b0..3679e66 100644
--- a/src/app/docs/page.test.tsx
+++ b/src/app/docs/page.test.tsx
@@ -21,6 +21,24 @@ describe('DocsPage', () => {
expect(screen.getByText(/GET \/api\/v1\/quote/i)).toBeInTheDocument();
});
+ it('forms a valid heading outline without skipping levels', () => {
+ const { container } = render();
+ const headings = Array.from(
+ container.querySelectorAll('h1, h2, h3, h4, h5, h6')
+ );
+ const levels = headings.map((h) => parseInt(h.tagName[1], 10));
+
+ expect(levels.length).toBeGreaterThan(0);
+ expect(levels[0]).toBe(1); // Page starts with h1
+
+ for (let i = 1; i < levels.length; i++) {
+ const prev = levels[i - 1];
+ const curr = levels[i];
+ // A heading can be any level up to prev + 1
+ expect(curr).toBeLessThanOrEqual(prev + 1);
+ }
+ });
+
it('marks openapi.json as an external link', () => {
render();
const link = screen.getByRole('link', { name: /openapi\.json/i });
diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx
index 93fae5d..5b82454 100644
--- a/src/app/docs/page.tsx
+++ b/src/app/docs/page.tsx
@@ -1,10 +1,11 @@
import type { Metadata } from 'next';
import { OpenApiLink } from './OpenApiLink';
+import { DocsSection } from "./Section";
export const dynamic = 'force-static';
export const metadata: Metadata = {
- title: 'Docs',
+ title: 'Docs — StableRoute',
description: 'Short reference for the StableRoute HTTP API common endpoints.',
};
@@ -42,16 +43,13 @@ export default function DocsPage() {
Companion to (opens external API spec) — short prose for
the most common endpoints.
-
+
{sections.map((s) => (
-
-
- {s.h}
- -
- {s.p}
-
-
+
+ {s.p}
+
))}
-
+
);
}