توافق WCAG 2.1 AA لمواقع التعليم: قائمة التحقق من Next.js
قائمة التحقق من توافق WCAG 2.1 AA لمواقع التعليم: Next.js
في عام 2024 وحده، استهدفت أكثر من 200 دعوى قضائية تتعلق بـ ADA المؤسسات التعليمية بسبب عدم إمكانية الوصول إلى مواقعها. تراوحت التسويات بين 25 ألف دولار و 300 ألف دولار فأكثر، وهذا قبل تكاليف المعالجة. موقع جامعتك أو منطقتك التعليمية هو هدف قانوني إذا لم يلبِ معايير WCAG 2.1 AA.
إليك الشيء الذي لن تخبرك به معظم الوكالات: نفس التكنولوجيا التي تجعل موقعك أسرع تجعله أيضاً أكثر إمكانية للوصول. موقع Next.js مبني بـ HTML دلالي و Tailwind CSS يحقق درجات إمكانية وصول Lighthouse بقيمة 95+ من الصندوق. موقع WordPress نموذجي يحتوي على 30 مكون إضافي؟ ينتهي به الحال في مكان ما بين 40 و 60. لقد قمت بتدقيق عدد كافٍ من مواقع التعليم بحيث أعرف أن اختيار المنصة الذي تتخذه اليوم يحدد ما إذا كنت تقدم تذاكر المعالجة أم تنام بسلام في العام القادم.
هذا ليس نظرة عامة نظرية. إنها قائمة تحقق تقنية عاملة -- ثماني فئات، أمثلة كود حقيقية، وأنماط التنفيذ الدقيقة التي نستخدمها عند بناء مواقع تعليمية يمكن الوصول إليها في Social Animal. ضع إشارة مرجعية. شاركها مع فريق التطوير الخاص بك. اطبعها والصقها على الجدار.
جدول المحتويات
- السياق القانوني: لماذا مواقع التعليم أهداف
- 1. HTML الدلالي
- 2. تسميات ARIA والمناطق المباشرة
- 3. التنقل عبر لوحة المفاتيح
- 4. تباين الألوان
- 5. نص بديل للصور
- 6. النماذج الميسرة
- 7. الفيديو والوسائط المتعددة
- 8. الاختبار المؤتمت في CI/CD
- لماذا يكافح WordPress و Drupal مع إمكانية الوصول
- ميزة Headless: إمكانية الوصول المفروضة وقت البناء
- الأسئلة الشائعة

السياق القانوني: لماذا مواقع التعليم أهداف
دعنا نتخلص من الأمور القانونية أولاً لأن هذا هو ما يجعل مدير الشؤون المالية الخاص بك ينتبه.
القسم 508 من قانون إعادة التأهيل ينطبق على جميع الوكالات الفيدرالية وكل مؤسسة تتلقى تمويلاً فيدرالياً. هذا هو كل جامعة عامة. كل منطقة تعليمية عامة. إذا أخذت مؤسستك دولاراً واحداً من أموال فيدرالية -- بما في ذلك منح Pell أو تمويل البحث أو أموال العنوان الأول -- ينطبق القسم 508 عليك.
ADA العنوان III يغطي أماكن الإقامة العامة. حكمت المحاكم باستمرار على أن هذا يشمل الجامعات الخاصة ومواقعها. تواجه Harvard و MIT وعدد لا يحصى من المؤسسات الخاصة الأصغر دعاوى قضائية بموجب العنوان III.
WCAG 2.1 AA هو المعيار التقني الذي تشير إليه المحاكم عند تقييم التوافقية. أصدرت وزارة العدل قاعدة نهائية في عام 2024 تنص بوضوح على أن مواقع حكومات الولايات والحكومات المحلية (بما في ذلك الجامعات العامة والمناطق التعليمية) يجب أن تتوافق مع مستوى WCAG 2.1 AA. هذا ليس اقتراح. إنها قاعدة بها مواعيد نهائية للإنفاذ.
تروي الأرقام القصة: زادت دعاوى ADA ضد المؤسسات التعليمية بنحو 300% بين عام 2018 و 2025. حلت مكتب الحقوق المدنية (OCR) أكثر من 15000 شكوى في السنة المالية 2024، مع أن إمكانية الوصول الرقمي تكون من أسرع الفئات المشتكى منها.
| الإطار القانوني | ينطبق على | المعيار | الموعد النهائي الرئيسي |
|---|---|---|---|
| القسم 508 | الجامعات العامة والمناطق التعليمية (متلقو التمويل الفيدرالي) | WCAG 2.1 AA | يمكن تطبيقه بالفعل |
| ADA العنوان II (قاعدة DOJ 2024) | كيانات حكومة الولاية والحكومة المحلية | WCAG 2.1 AA | أبريل 2026 (الكيانات الكبيرة)، أبريل 2027 (الصغيرة) |
| ADA العنوان III | الجامعات الخاصة، المدارس الخاصة K-12 | WCAG 2.1 AA (الواقع) | المحاكم تفرض الآن |
| قوانين الولايات (كاليفورنيا، نيويورك، إلخ.) | يختلف حسب الولاية | WCAG 2.1 AA نموذجي | يختلف |
الآن دعنا نبني موقعاً يمر بالفعل.
1. HTML الدلالي
HTML الدلالي هو أساس كل شيء. إذا أخطأت في هذا، فلن تنقذك أي كمية من سمات ARIA. يعتمد قارئو الشاشة على الهيكل الدلالي للمستند لمساعدة المستخدمين على فهم تسلسل الصفحة والتنقل بين الأقسام.
تسلسل العنوانات
تحصل كل صفحة على <h1> واحد بالضبط. تتبع العناوين الفرعية بالترتيب: <h2>، ثم <h3>، ثم <h4>. لا تتخط المستويات أبداً. يفقد مستخدم قارئ الشاشة السياق عندما يسمع "مستوى العنوان 4" بعد "مستوى العنوان 2".
أرى مواقع التعليم تنتهك هذه القاعدة باستمرار -- خاصة على صفحات الأقسام حيث ألصق شخص ما في CMS محتوى يحتوي على مستويات عنوان عشوائية لأن حجم الخط بدا صحيحاً بصرياً.
عناصر معالم الصفحة
استخدم <nav> و <main> و <aside> و <footer> و <header>. تنشئ هذه مناطق قابلة للتنقل لمستخدمي قارئ الشاشة. يمكن لمستخدم JAWS أو NVDA الضغط على مفتاح واحد للقفز بين المعالم.
الأزرار مقابل الروابط
هذا يرفعني من الجدران. استخدم <button> للعناصر التفاعلية التي تؤدي إجراء (افتح قائمة، أرسل نموذج، بدّل عامل تصفية). استخدم <a> للتنقل الذي ينقل المستخدم إلى صفحة أو قسم جديد. لا تستخدم <div> مع معالج onClick أبداً.
// ❌ خطأ: div يتظاهر بأنه زر
<div onClick={handleClick} className="cursor-pointer">
Open Menu
</div>
// ❌ خطأ: زر مستخدم للتنقل
<button onClick={() => router.push('/admissions')}>
View Admissions
</button>
// ✅ صحيح: زر دلالي للإجراءات
<button
onClick={handleMenuToggle}
aria-expanded={isOpen}
aria-controls="main-nav"
className="focus-visible:ring-2 focus-visible:ring-offset-2"
>
Open Menu
</button>
// ✅ صحيح: anchor للتنقل
import Link from 'next/link';
<Link href="/admissions" className="focus-visible:ring-2">
View Admissions
</Link>
إليك مكون ملاحة ميسور كامل لموقع جامعة:
// components/MainNav.tsx
import Link from 'next/link';
export function MainNav() {
return (
<header role="banner">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:text-lg"
>
Skip to main content
</a>
<nav aria-label="Main navigation">
<ul role="list">
<li><Link href="/admissions">Admissions</Link></li>
<li><Link href="/academics">Academics</Link></li>
<li><Link href="/campus-life">Campus Life</Link></li>
<li><Link href="/research">Research</Link></li>
<li><Link href="/about">About</Link></li>
</ul>
</nav>
</header>
);
}
Next.js له ميزة طبيعية هنا. لأنك تكتب React/JSX، فأنت تؤلف العناصر الدلالية مباشرة. لا توجد أداة سحب وإفلات لإنشاء صفحات تولد حساء <div> المتداخل خلف الكواليس.
2. تسميات ARIA والمناطق المباشرة
ARIA (تطبيقات الويب الغنية بإمكانية الوصول) تملأ الفجوات حيث تكون الدلالات HTML الأصلية غير كافية. لكن إليك القاعدة الذهبية: لا ARIA أفضل من ARIA سيء. استخدم عناصر HTML الأصلية أولاً. استخدم ARIA فقط عندما تحتاج إليها.
متى تستخدم ARIA
aria-label: للأزرار التي تحتوي على أيقونات فقط حيث لا توجد نصوص مرئية. يحتاج زر البحث بعلامة العدسة المكبرة علىaria-label="Search".aria-describedby: يربط الإدخال برسالة الخطأ الخاصة به حتى يقرأ قارئو الشاشة كليهما.aria-live="polite": يعلن عن تحديثات المحتوى الديناميكي (نتائج البحث الجاري تحميلها، تغييرات عامل التصفية) دون سرقة التركيز.role="alert": لرسائل الخطأ العاجلة التي تحتاج إلى إعلان فوري.aria-expanded: ينقل ما إذا كان الأكورديون أو القائمة المنسدلة أو القائمة مفتوحة أم مغلقة.
// components/SearchBar.tsx
import { useState, useRef } from 'react';
export function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isSearching, setIsSearching] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
setIsSearching(true);
// Fetch results from your API
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data.results);
setIsSearching(false);
}
return (
<div role="search" aria-label="Site search">
<form onSubmit={handleSearch}>
<label htmlFor="site-search" className="sr-only">
Search the university website
</label>
<input
ref={inputRef}
id="site-search"
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search programs, faculty, news..."
autoComplete="off"
className="focus-visible:ring-2 focus-visible:ring-blue-600"
/>
<button type="submit" aria-label="Submit search">
<SearchIcon aria-hidden="true" />
</button>
</form>
{/* Live region announces results to screen readers */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{isSearching
? 'Searching...'
: `${results.length} results found for ${query}`
}
</div>
{results.length > 0 && (
<ul role="list" aria-label="Search results">
{results.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
)}
</div>
);
}
لاحظ منطقة aria-live="polite". عندما تحمل نتائج البحث، يسمع مستخدم قارئ الشاشة "وجدت 5 نتائج لعلوم الحاسوب" دون الاضطرار إلى الانتقال إلى قائمة النتائج. هذا هو الفرق بين موقع يمكن الوصول إليه وموقع غير قابل للاستخدام.

3. التنقل عبر لوحة المفاتيح
إذا كان يمكن للمستخدم الذي يرى البصر النقر عليه، فيجب أن يتمكن مستخدم لوحة المفاتيح من الوصول إليه باستخدام Tab وتفعيله باستخدام Enter أو Space. بلا استثناءات.
الأمور غير القابلة للتفاوض
- كل عنصر تفاعلي يمكن الوصول إليه عبر Tab. يحدث هذا تلقائياً إذا كنت تستخدم عناصر
<button>و<a>دلالية. - ترتيب Tab منطقي. يجب أن يتبع ترتيب Tab تدفق المحتوى البصري. لا تستخدم قيم
tabindexأكبر من 0 -- فهي تخلق الفوضى. - مؤشرات التركيز المرئية. لا تكتب
outline: noneأوoutline: 0على:focusأبداً. أبداً. استخدمfocus-visible:ring-2من Tailwind بدلاً من ذلك -- يظهر الحلقة لمستخدمي لوحة المفاتيح ولكن ليس لنقرات الماوس. - روابط التخطي. يجب أن تكون أول عنصر قابل للتركيز على الصفحة رابط "تخطي إلى المحتوى الرئيسي". مخفي حتى يتم التركيز عليه.
- احتجاز التركيز في النوافذ المنبثقة. عند فتح نافذة منبثقة، يجب أن يدور Tab ضمن النافذة المنبثقة. يغلقها Escape. يعود التركيز إلى زر الزناد عند إغلاقه.
// components/AccessibleModal.tsx
import { useEffect, useRef, useCallback } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
triggerRef: React.RefObject<HTMLButtonElement>;
}
export function AccessibleModal({ isOpen, onClose, title, children, triggerRef }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const trapFocus = useCallback((e: KeyboardEvent) => {
if (!modalRef.current) return;
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstEl = focusableElements[0] as HTMLElement;
const lastEl = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstEl) {
e.preventDefault();
lastEl.focus();
} else if (!e.shiftKey && document.activeElement === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
}, [onClose]);
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus();
document.addEventListener('keydown', trapFocus);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', trapFocus);
document.body.style.overflow = '';
// Return focus to trigger
if (!isOpen) triggerRef.current?.focus();
};
}, [isOpen, trapFocus, triggerRef]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
>
<div className="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h2 id="modal-title" className="text-xl font-bold">{title}</h2>
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Close dialog"
className="focus-visible:ring-2 focus-visible:ring-blue-600 rounded p-1"
>
✕
</button>
</div>
{children}
</div>
</div>
);
}
يتعامل هذا النمط مع احتجاز التركيز، Escape للإغلاق، واستعادة التركيز. لقد قمت بشحن تنويعات من هذا على ما لا يقل عن عشرة مواقع تعليمية.
4. تباين الألوان
نسب التباين هي من أكثر معايير WCAG فشلاً بشكل متكرر -- وأحدها يسهل إصلاحه إذا قمت بإعداد رموز التصميم الخاصة بك بشكل صحيح من البداية.
| نوع النص | الحد الأدنى لنسبة التباين (AA) | مثال |
|---|---|---|
| نص عادي (< 18px) | 4.5:1 | نص الجسم والتعليقات والعلامات النموذجية |
| نص كبير (18px+ عادي، 14px+ غامق) | 3:1 | العناوين والأزرار الكبيرة |
| العناصر التفاعلية | 3:1 مقابل الألوان المجاورة | حدود حقول النموذج وشرطة الروابط |
| العناصر غير النصية (الأيقونات وحلقات التركيز) | 3:1 | حدود حقول النموذج وعناصر الرسم البياني |
قواعس بخلاف النسب
لا تستخدم اللون وحده لنقل المعلومات. لا يمكن أن تكون حالة الخطأ مجرد "الحقل يتحول إلى أحمر." يحتاج إلى حد أحمر + أيقونة خطأ + تسمية نصية. لا يمكن لتصور البيانات الاعتماد فقط على اللون للتمييز بين الفئات -- استخدم الأنماط أو التسميات أو الأشكال المميزة.
أدوات الاختبار
- Chrome DevTools Accessibility Panel: افحص أي عنصر وأرى نسبة التباين الخاصة به على الفور.
- WebAIM Contrast Checker: أدخل القيم السادسة عشرية واحصل على التمرير/الرسوب بشأن AA و AAA.
- Figma plugins (Stark و A11y): اكتشف مشاكل التباين قبل وصولها إلى الكود.
كمرجع: لون ذهبي فاتح (#c8a96e) على خلفية شبه سوداء (#0a0a0b) ينتج نسبة تباين 4.7:1 -- يمرر AA للنص العادي. رموز التصميم مهمة.
5. نص بديل للصور
تحتاج كل عنصر <img> إلى سمة alt. ما الذي يدخل فيه يعتمد على غرض الصورة.
شجرة القرار
- الصور الإعلامية (الصور الفوتوغرافية والرسوم التوضيحية التي تنقل محتوى): اكتب نصاً بديلاً وصفياً. "طلاب يدرسون في مكتبة الحرم الجامعي" وليس "image123.jpg".
- الصور الزخرفية (نسيج الخلفية والفواصل البصرية والجماليات البحتة): استخدم
alt=""(سلسلة فارغة). يخبر هذا قارئي الشاشة بتخطيها. - الرسوم البيانية والرسوم البيانية: إما اكتب نصاً بديلاً مفصلاً يلخص البيانات أو استخدم
aria-describedbyالذي يشير إلى جدول بيانات تحت الرسم البياني. - صور الأساتذة:
alt="الدكتورة سارة تشن، أستاذة مشاركة، قسم علوم الحاسوب" - صور بطل البرنامج: صف السياق المشهد.
alt="طلاب الهندسة يتعاونون على مشروع الروبوتات في مختبر Maker"
// ✅ صورة إعلامية
import Image from 'next/image';
<Image
src="/campus/library-study-area.jpg"
alt="Students studying at tables in the three-story Founders Library atrium"
width={1200}
height={600}
/>
// ✅ صورة زخرفية
<Image
src="/patterns/wave-divider.svg"
alt=""
role="presentation"
width={1200}
height={40}
/>
// ✅ صورة أستاذ
<Image
src="/faculty/sarah-chen.jpg"
alt="Dr. Sarah Chen, Associate Professor, Department of Computer Science"
width={300}
height={400}
/>
يحذرك مكون Next.js Image في الواقع إذا نسيت سمة alt. الأشياء الصغيرة مثل هذا تضيف.
6. النماذج الميسرة
النماذج هي حيث تعيش المواقع التعليمية والموت على إمكانية الوصول. نماذج التطبيق وضحايا نماذج الاتصال وتسجيل المسار والمساعدة المالية -- إذا لم تكن هذه يمكن الوصول إليها، فأنت تستبعد الطلاب الذين يحتاجون إلى خدماتك بشكل أكثر.
// components/ContactForm.tsx
import { useState } from 'react';
export function ContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitted, setSubmitted] = useState(false);
function validate(formData: FormData) {
const newErrors: Record<string, string> = {};
if (!formData.get('name')) newErrors.name = 'Full name is required.';
if (!formData.get('email')) newErrors.email = 'Email address is required.';
const email = formData.get('email') as string;
if (email && !email.includes('@')) newErrors.email = 'Please enter a valid email address.';
if (!formData.get('message')) newErrors.message = 'Message is required.';
return newErrors;
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newErrors = validate(formData);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) setSubmitted(true);
}
if (submitted) {
return <div role="alert"><p>Thank you! We'll be in touch within 2 business days.</p></div>;
}
return (
<form onSubmit={handleSubmit} noValidate>
{/* Error summary */}
{Object.keys(errors).length > 0 && (
<div role="alert" className="bg-red-50 border-red-500 border p-4 mb-6 rounded">
<h2 className="text-red-800 font-bold mb-2">
Please fix {Object.keys(errors).length} error(s):
</h2>
<ul>
{Object.entries(errors).map(([field, msg]) => (
<li key={field}>
<a href={`#field-${field}`} className="text-red-700 underline">{msg}</a>
</li>
))}
</ul>
</div>
)}
<div className="mb-4">
<label htmlFor="field-name" className="block font-medium mb-1">
Full Name <span aria-hidden="true">*</span>
</label>
<input
id="field-name"
name="name"
type="text"
autoComplete="name"
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'error-name' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.name && (
<p id="error-name" className="text-red-600 text-sm mt-1" role="alert">
{errors.name}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="field-email" className="block font-medium mb-1">
Email Address <span aria-hidden="true">*</span>
</label>
<input
id="field-email"
name="email"
type="email"
autoComplete="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'error-email' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.email && (
<p id="error-email" className="text-red-600 text-sm mt-1" role="alert">
{errors.email}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="field-message" className="block font-medium mb-1">
Message <span aria-hidden="true">*</span>
</label>
<textarea
id="field-message"
name="message"
rows={5}
aria-required="true"
aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'error-message' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.message && (
<p id="error-message" className="text-red-600 text-sm mt-1" role="alert">
{errors.message}
</p>
)}
</div>
<button
type="submit"
className="bg-blue-700 text-white px-6 py-3 rounded font-medium hover:bg-blue-800 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-600"
>
Send Message
</button>
</form>
);
}
لاحظ ملخص الخطأ في الأعلى مع روابط العنصر إلى كل حقل مشكلة. اتصالات aria-describedby. سمات autoComplete. حالات aria-required و aria-invalid. هذا هو ما يبدو عليه النموذج الميسر فعلاً.
7. الفيديو والوسائط المتعددة
مواقع الجامعات مليئة بالفيديو -- جولات الحرم الجامعي الافتراضية وتسجيلات المحاضرات وخطابات الرئيس وشهادات الطلاب. تحتاج كل واحدة منها إلى بدائل ميسرة.
المتطلبات
- التسميات التوضيحية لجميع محتوى الفيديو. التسميات التوضيحية ذاتية التوليد (YouTube وRev.ai) هي نقطة البداية، لكن يجب أن يراجعها الإنسان. التسميات التوضيحية ذاتية التوليد لها معدلات خطأ 10-15٪ -- غير مقبول للمحتوى الأكاديمي.
- الأوصاف الصوتية للمحتوى البصري فقط. فيديو جولتك الافتراضية للحرم الجامعي يوضح المباني الجميلة؟ يسمع المستخدم الأعمى الصمت ما لم تروي ما هو على الشاشة.
- نصوص متاحة لجميع الوسائط المتعددة. نسخة نصية قابلة للتنزيل أو في الصفحة.
- تحكم في الإيقاف المؤقت والإيقاف لأي محتوى يتم تشغيله تلقائياً.
- لا تشغيل صوتي تلقائي لا يمكن للمستخدمين إيقافه على الفور.
// نمط embedded فيديو ميسور
<figure>
<div className="relative aspect-video">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID?cc_load_policy=1"
title="Virtual tour of the Engineering Building, including labs, classrooms, and student spaces"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="absolute inset-0 w-full h-full"
/>
</div>
<figcaption className="mt-2 text-sm text-gray-600">
Virtual tour of the Engineering Building.
<a href="/transcripts/engineering-tour" className="underline ml-1">
Read the full transcript
</a>
</figcaption>
</figure>
سمة title على iframe حرجة -- قارئو الشاشة يعلنونها عندما يصل المستخدم إلى المضمن. معامل cc_load_policy=1 يفرض التسميات التوضيحية بشكل افتراضي في YouTube embeds.
8. الاختبار المؤتمت في CI/CD
اختبار إمكانية الوصول اليدوي ضروري ولكن غير كافٍ. تحتاج إلى فحوصات مؤتمتة تمنع الانحدارات من الوصول إلى الإنتاج أبداً.
خط الأنابيب
- Lighthouse CI في GitHub Actions أو بناء Vercel الخاص بك: ضع عتبة وفشل البناء إذا انخفضت درجة إمكانية الوصول أسفل 90.
- تكامل axe-core: قم بتشغيل فحوصات WCAG 2.1 AA الآلية على كل مكون أثناء اختبارات الوحدة/التكامل.
- اختبار لوحة المفاتيح اليدوي: قبل كل إصدار رئيسي، تنقل عبر الموقع بالكامل باستخدام Tab و Enter و Space والمفاتيح المختلفة فقط.
- اختبار قارئ الشاشة: اختبار فصلي مع VoiceOver (Mac) و NVDA (Windows) و TalkBack (Android).
# .github/workflows/accessibility.yml
name: Accessibility Audit
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000/
http://localhost:3000/admissions
http://localhost:3000/academics
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
// lighthouse-budget.json
[{
"path": "/*",
"options": {
"assertions": {
"categories:accessibility": ["error", { "minScore": 0.9 }]
}
}
}]
لاختبار المكون على مستوى مع axe-core:
// __tests__/ContactForm.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { ContactForm } from '../components/ContactForm';
expect.extend(toHaveNoViolations);
test('ContactForm has no accessibility violations', async () => {
const { container } = render(<ContactForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
لا تنشر أبداً صفحة غير ميسرة. ابنِ الحراس في خط الأنابيب الخاص بك، ولن تحتاج إلى ذلك.
لماذا يكافح WordPress و Drupal مع إمكانية الوصول
أنا لا أقول أن WordPress لا يمكن أن يكون ميسراً. أنا أقول في الممارسة العملية، فهو نادراً ما يكون كذلك -- خاصة بالنسبة لمواقع التعليم ذات المتطلبات المعقدة.
إليك السبب:
- إمكانية الوصول تعتمد على كل مكون إضافي مثبت أن يكون ميسراً. نموذج الاتصال الإضافي الخاص بك وإضافة تقويم الأحداث الخاصة بك والمكون الإضافي mega menu الخاص بك والمكون الإضافي slider الخاص بك -- يجب أن يكون كل واحد منها ينتج عنه علامات صحيحة وقابلة للوصول. معظمهم لا يفعلون ذلك.
- تحديثات المكون الإضافي تكسر الأشياء. قد يؤدي تحديث WooCommerce أو تحديث Elementor إلى إدخال انحدارات إمكانية الوصول بسرية. لن تعرف حتى يشتكي شخص ما -- أو يقاضيك.
- لا يوجد فحص إمكانية وصول آلي في خط أنابيب النشر. لا تتضمن عمليات نشر WordPress القياسية أبواباً Lighthouse أو فحوصات axe-core. تذهب التغييرات مباشرة دون أي تحقق من إمكانية الوصول.
- يقوم مؤلفو المحتوى بإنشاء محتوى غير ميسور. تتيح محررات WYSIWYG للمستخدمين تخطي مستويات العناوين وإدراج الصور بدون نص بديل وإنشاء روابط تقول "انقر هنا". لا توجد آلية فرض.
لقد قمت بتدقيق مواقع WordPress التعليمية التي حققت Lighthouse 42 بعد تحديث المظهر. 42. لم تكن المدرسة تعرف حتى أخبرناها.
ميزة Headless: إمكانية الوصول المفروضة وقت البناء
يقلب الأسلوب الذي نتخذه مع تطوير Next.js و معمارية CMS بلا رأس النموذج. لا يتم إصلاح إمكانية الوصول بعد ذلك -- يتم فرضها وقت البناء.
| النهج | فرض إمكانية الوصول | درجة Lighthouse النموذجية | مخاطر الانحدار |
|---|---|---|---|
| WordPress + plugins | التدقيق اليدوي وأدوات الطبقة الشفافة | 40-65 | مرتفع (كل تحديث مكون إضافي) |
| Drupal + contrib modules | أفضل من WP، لا يزال يدوياً | 55-75 | متوسط |
| Next.js + headless CMS | الأتمتة CI/CD والبناء وقت | 90-100 | منخفض (أبواب آلية) |
HTML الدلالي هو افتراضي React. تعتبر منفعات focus-visible من Tailwind مجرد فئة واحدة. فحص Lighthouse من CI/CD يمنع الانحدارات. قاعدة كود واحدة تعني توافقية متسقة عبر كل صفحة مدرسة وقسم وبرنامج -- وليس 47 تثبيتاً مختلفاً لـ WordPress بـ 47 تكوين مكون إضافي مختلف.
إذا كنت تفكر في إعادة بناء أو هجرة، فسنكون سعداء بالحديث عن التفاصيل المحددة. تحقق من الخاص بنا capabilities أو تواصل معنا. وإذا كنت فضولياً حول ما يبدو عليه مشروع موقع تعليمي بلا رأس من منظور الميزانية، فإن صفحة التسعير الخاصة بنا بها أرقام شفافة.
الأسئلة الشائعة
هل ينطبق توافق WCAG 2.1 AA على جميع مواقع المدارس، بما في ذلك K-12؟ نعم. تتلقى المناطق التعليمية العامة التمويل الفيدرالي، مما يؤدي إلى متطلبات القسم 508. تغطي القاعدة النهائية لـ DOJ لعام 2024 بموجب ADA العنوان II كيانات حكومة الولاية والحكومة المحلية، والتي تشمل المناطق التعليمية العامة. قد تكون المدارس الخاصة K-12 مغطاة أيضاً بموجب ADA العنوان III. الافتراض الآمن: إذا كنت تدير موقع مدرسة، ينطبق WCAG 2.1 AA عليك.
ما الفرق بين WCAG 2.1 AA و WCAG 2.2 AA؟ WCAG 2.2، المنشورة في أكتوبر 2023، تضيف تسع معايير نجاح جديدة بالإضافة إلى 2.1. تشير قاعدة DOJ 2024 بشكل خاص إلى WCAG 2.1 AA كمعيار الامتثال الآن. ومع ذلك، فإن الهدف لـ 2.2 AA هو تخطيط أمامي ذكي. تركز المعايير الجديدة على أشياء مثل ظهور التركيز والحركات السحب والمساعدة المتسقة -- جميعها ذات صلة بمواقع التعليم ذات النماذج المعقدة والملاحة.
هل يمكن لأداة تراكب إمكانية الوصول مثل accessiBe أو UserWay أن تجعل موقعنا متوافقاً؟ لا. اتحاد الرابطة الوطنية للعميان والعديد من قرارات المحاكم قد أفادت بأن أدوات التراكب لا توفر توافقية WCAG. في الواقع، استشهد بعض المدعين بوجود أدوات التراكب كدليل على أن المدعى عليه يعرف أن موقعه كان غير ميسور لكنه اختار إصلاح تجميلي بدلاً من إصلاح حقيقي. أصلح الكود المصدر.
كم تكلفة معالجة موقع تعليمي موجود لـ WCAG 2.1 AA؟ تختلف تكاليف المعالجة بشكل كبير حسب الحالة الحالية للموقع. بالنسبة لموقع WordPress جامعي نموذجي، توقع 50 ألف دولار إلى 150 ألف دولار فأكثر لمعالجة شاملة. تجد العديد من المؤسسات أنه من الأكثر فعالية من حيث التكلفة إعادة البناء على مكدس حديث ويمكن الوصول إليه بشكل افتراضي مثل Next.js، حيث تتضمن تكلفة المشروع الإجمالية (75 ألف دولار إلى 200 ألف دولار) توافقاً كاملاً مع WCAG من اليوم الأول بالإضافة إلى تكاليف صيانة جارية أقل بكثير.
ما درجة Lighthouse إمكانية الوصول التي يجب أن نستهدفها؟ الحد الأدنى 90، الهدف 95+. لكن افهم أن Lighthouse يمكنه فقط اكتشاف حوالي 30-40٪ من مشاكل WCAG 2.1 AA. يمكنه التحقق من نسب التباين ووجود النص البديل وصحة سمة ARIA، لكنه لا يمكنه الاختبار ما إذا كان ترتيب Tab الخاص بك منطقياً أو ما إذا كانت روابط التخطي الخاصة بك تعمل أو ما إذا كان المحتوى الخاص بك منطقياً لمستخدم قارئ الشاشة. الاختبار المؤتمت بالإضافة إلى الاختبار اليدوي هو الإجابة الوحيدة الحقيقية.
كم مرة يجب أن نختبر موقع التعليم الخاص بنا بحثاً عن إمكانية الوصول؟ يجب أن يتم الاختبار الآلي على كل طلب سحب -- هذا ما خط أنابيب CI/CD موجود له. يجب أن يحدث اختبار التنقل اليدوي لوحة المفاتيح قبل كل إصدار رئيسي. اختبار قارئ الشاشة (VoiceOver و NVDA) يجب أن يحدث كل ربع سنة على الأقل. يجب أن يحدث تدقيق WCAG مهني كامل سنوياً أو عندما تتم إضافة ميزات كبيرة.
هل Next.js تجعل الموقع ميسراً تلقائياً؟
لا يوجد إطار عمل ميسر تلقائياً -- لا يزال يتعين عليك كتابة كود جيد. لكن Next.js يوفر مزايا كبيرة: مكون Image يحذر من النص البديل المفقود وجزء Link ينتج عنه علامات <a> مناسبة مع معالجة href صحيحة وـ React JSX يشجع العناصر الدلالية وخط أنابيب البناء يدعم اختبار إمكانية الوصول المؤتمتة. لا يقوم الإطار بالعمل من أجلك، لكنه يجعل فعل الشيء الصحيح هو سبيل المقاومة الأقل.
ما هي العقوبات لعدم امتلاك موقع جامعة غير ميسور؟ تراوحت تسويات ADA لمواقع التعليم بين 25000 دولار إلى أكثر من 300000 دولار، بالإضافة إلى رسوم المحامي، بالإضافة إلى تكلفة المعالجة (التي قد تتجاوز التسوية نفسها). بخلاف العقوبات النقدية، يمكن لـ OCR أن تطلب اتفاقيات الامتثال التي تفرض المراقبة والإبلاغ المستمرة لسنوات. وهناك الضرر على السمعة -- النوع الذي يجعل الطلاب المحتملين وأعضاء هيئة التدريس يفكرون مرتين في مؤسستك.