Skip to content

Refactor lint.yml comments and accessibility check #8

Refactor lint.yml comments and accessibility check

Refactor lint.yml comments and accessibility check #8

Workflow file for this run

name: Lint & Quality Check
on:
push:
branches: [main]
jobs:
lint:
name: HTML · CSS · Accessibility · Links
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# 1. HTML VALIDITY
- name: Validate HTML (W3C)
uses: Cyb3r-Jak3/html5validator-action@v7.2.0
with:
root: .
css: false
format: text
# 2. CSS VALIDITY
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install stylelint
run: npm install -g stylelint stylelint-config-standard
- name: Write stylelint config
run: |
cat > .stylelintrc.json << 'EOF'
{
"extends": "stylelint-config-standard",
"rules": {
"no-descending-specificity": null,
"alpha-value-notation": null,
"color-function-notation": null,
"color-function-alias-notation": null,
"color-hex-length": null,
"declaration-block-single-line-max-declarations": null,
"keyframes-name-pattern": null,
"media-feature-range-notation": null
}
}
EOF
- name: Extract inline CSS and lint
run: |
node - << 'EOF'
const fs = require('fs');
const html = fs.readFileSync('index.html', 'utf8');
const match = html.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
if (match) {
fs.writeFileSync('_extracted.css', match[1]);
console.log('CSS extracted successfully');
} else {
console.log('No inline <style> block found');
fs.writeFileSync('_extracted.css', '');
}
EOF
stylelint _extracted.css --allow-empty-input
rm -f _extracted.css
# 3. ACCESSIBILITY (pure Node, no browser needed)
- name: Run accessibility check
run: |
node - << 'EOF'
const fs = require('fs');
const html = fs.readFileSync('index.html', 'utf8');
let errors = [];
let passes = [];
// 1. lang attribute on <html>
if (/<html[^>]+lang=["'][a-z]+["']/i.test(html)) {
passes.push('✅ <html> has lang attribute');
} else {
errors.push('❌ <html> missing lang attribute (WCAG 3.1.1)');
}
// 2. <title> exists and is non-empty
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
if (titleMatch && titleMatch[1].trim()) {
passes.push('✅ <title> present: "' + titleMatch[1].trim() + '"');
} else {
errors.push('❌ Missing or empty <title> (WCAG 2.4.2)');
}
// 3. All <a> tags have non-empty text or aria-label
const anchors = [...html.matchAll(/<a\s[^>]*>([\s\S]*?)<\/a>/gi)];
anchors.forEach(match => {
const tag = match[0];
const inner = match[1].replace(/<[^>]+>/g, '').trim();
const hasAriaLabel = /aria-label=["'][^"']+["']/i.test(tag);
if (!inner && !hasAriaLabel) {
errors.push('❌ <a> with no text or aria-label (WCAG 2.4.4): ' + tag.slice(0, 80));
}
});
if (anchors.length > 0 && errors.filter(e => e.includes('2.4.4')).length === 0) {
passes.push('✅ All ' + anchors.length + ' links have accessible text');
}
// 4. No duplicate IDs
const ids = [...html.matchAll(/\sid=["']([^"']+)["']/gi)].map(m => m[1]);
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
if (dupes.length > 0) {
errors.push('❌ Duplicate IDs found: ' + dupes.join(', ') + ' (WCAG 4.1.1)');
} else {
passes.push('✅ No duplicate IDs');
}
// 5. All <img> tags have alt attributes
const imgs = [...html.matchAll(/<img\s[^>]*>/gi)];
imgs.forEach(match => {
if (!/alt=["'][^"']*["']/i.test(match[0])) {
errors.push('❌ <img> missing alt attribute (WCAG 1.1.1): ' + match[0].slice(0, 80));
}
});
if (imgs.length === 0) passes.push('✅ No images without alt (no images present)');
// 6. target="_blank" links have rel="noopener"
const blankLinks = [...html.matchAll(/<a\s[^>]*target=["']_blank["'][^>]*>/gi)];
blankLinks.forEach(match => {
if (!/rel=["'][^"']*(noopener|noreferrer)[^"']*["']/i.test(match[0])) {
errors.push('❌ target="_blank" link missing rel="noopener": ' + match[0].slice(0, 80));
}
});
if (blankLinks.length > 0 && errors.filter(e => e.includes('noopener')).length === 0) {
passes.push('✅ All ' + blankLinks.length + ' external links have rel="noopener"');
}
// Report
console.log('\n── Accessibility Check ──────────────────\n');
passes.forEach(p => console.log(p));
if (errors.length > 0) {
console.log('');
errors.forEach(e => console.error(e));
console.log('\n❌ ' + errors.length + ' violation(s) found');
process.exit(1);
} else {
console.log('\n✅ All checks passed!');
process.exit(0);
}
EOF
# 4. BROKEN LINKS
- name: Check broken links (lychee)
uses: lycheeverse/lychee-action@v1
with:
args: >
--verbose
--no-progress
--exclude "mailto:"
--exclude "localhost"
--timeout 20
./index.html
fail: true