Accessibility audit, performance check and deploy
The site works on your laptop. This lesson is proving it works for everybody else, and then putting it somewhere real.
Run the audit before deploying. Fixing something on your machine takes a minute; fixing it after you have sent the link to a client takes a minute and an apology.
The audit, in four passes
Module 5's structure, applied to a finished site. About forty minutes in total.
Pass 1: the keyboard, three minutes
Put the mouse away. Tab from the top of each page.
□ The skip link is the first thing focused, and it becomes visible
□ It actually moves focus into <main>
□ Every link, button and field is reachable
□ A visible focus ring on every single stop
□ The order matches the page, with no jumping
□ Enter works on links; Enter and Space work on buttons
□ Nothing ever sits hidden under the sticky header
□ Tab all the way round and back to the address bar — no trap
The reference site has 16 stops on the home page, every one with a ring, in the order: skip link, logo, four nav links, the header button, the two hero buttons, the in-content links, the footer links — and then back to the skip link. That wrap-around is the proof there is no trap.
Pass 2: automated, three minutes
Run axe DevTools or Lighthouse on every page, and — the part people miss — in every state. For this site that means the form in its error state too: fill in a bad phone number, blur it, and scan again.
These catch about a third of issues: missing alt, unlabelled fields, contrast between
declared colours, duplicate ids, invalid ARIA.
Pass 3: a screen reader, fifteen minutes
The pass people skip and the one that changes how you write markup.
□ Pull up the headings list. Does it summarise the page?
□ Pull up the landmarks. Is there a main? Are the navs distinguishable?
□ Pull up the links. Do they make sense out of context?
□ Tab the form. Is every field named, and is the hint read?
□ Land on a timetable cell. Do you hear the day and the time, or just "Hatha"?
□ Close your eyes and understand the image sections from alt text alone
That timetable check is the one that justifies scope. With it you hear "Wednesday,
8:00, Pranayama". Without it, "Pranayama", and no way to know which class that is.
Pass 4: the emulations, five minutes
In the devtools Rendering panel:
□ prefers-color-scheme: dark
□ prefers-reduced-motion: reduce
□ forced-colors: active
□ achromatopsia — if it works with no colour at all, it works for every deficiency
And without devtools: 320px, 400% zoom, stylesheet off, images off.
Measure, do not look
The three defects in this build were all invisible and all found by measuring. Two snippets worth keeping.
Contrast, including over a photograph. The simple version walks up the DOM for a background colour — which returns white for text sitting on an image, and tells you nothing. For text over a photograph you have to sample the composited pixels:
// draw the image as object-fit: cover renders it, apply the scrim, then
// find the WORST pixel under each piece of text
const worst = Math.min(...pixels.map(p => ratio(textColour, p)));
The reference site's eyebrow measured 3.28:1 doing this, against a flat estimate that
looked fine. After darkening the scrim's weak end from 0.35 to 0.55 it cleared.
Re-measure when the photograph changes. When the placeholders were later replaced with real photographs, the same measurement was run again across eight widths — the eyebrow is always the worst element, and its worst pixel ranges from 5.03:1 at 320px to 9.45:1 at 1920px. Still above 4.5, so the scrim was left alone. A different photograph could easily have failed, and the only way to know is to measure again.
Every boundary, in both modes. Non-text contrast is 3:1, and the two modes are not symmetrical:
[...document.querySelectorAll('input, select, textarea')]
.map(f => ratio(getComputedStyle(f).borderTopColor, bgOf(f)));
Light mode: #767676 → 4.54:1. Dark mode: #4b5658 → 2.36:1, a failure, chosen
by eye by somebody who had just written the lesson about it. Raised to #667376 →
3.64:1.
Run both against your own site. They take a minute and they find things looking cannot.
Performance
□ Throttle to Slow 4G with 4× CPU, then reload
□ Network tab sorted by size — justify the largest file
□ Lighthouse, on the deployed URL rather than localhost
□ Under 500 KB and under 20 requests on first view
The reference home page, measured on the deployed site with an empty cache:
4 requests and 87.4 KB initially, rising to 8 requests and 187.8 KB once scrolled to
the bottom — the difference being the lazy-loaded images doing their job. The largest
transfer is hero-2000.webp at 80 KB.
Localhost has no latency, so the honest number comes from the deployed site under throttling.
The pre-deploy checklist
Content
□ No lorem ipsum, no TODO, no placeholder phone number
□ Unique specific <title> and description on every page
□ A favicon, or a 404 on every page load
□ og:image, or a grey box when shared on WhatsApp
Links and paths
□ Every internal link works from every page
□ Every image loads — check the Network tab, not your eyes
□ Filenames lowercase with hyphens
Housekeeping
□ No .DS_Store, no commented-out experiments
□ <html lang> set
□ A custom 404.html
The filename rule is the one that bites. Linux servers are case-sensitive and your
Mac is not, so About.html works locally and 404s live. It is the most common first
deployment failure and it costs nothing to avoid.
Deploying
Git first, then one of three hosts. All free, all deploy from Git, all give you HTTPS.
git init
git add .
git commit -m "The studio site"
git branch -M main
git remote add origin https://github.com/yourname/aarambh-yoga.git
git push -u origin main
With a .gitignore:
.DS_Store
Thumbs.db
*.log
.env
GitHub Pages
Settings → Pages → deploy from main, root folder. A minute later it is live at
yourname.github.io/aarambh-yoga.
Two things, both from module 6:
It serves from a subpath. So /about.html resolves to yourname.github.io/about.html
and breaks. The reference site uses relative links throughout — about.html,
images/hero-800.jpg — which is why it works on a subpath with no changes. If you used
root-relative links, this is where you find out.
Add an empty .nojekyll at the root, or GitHub runs the site through Jekyll and
ignores anything in a folder starting with an underscore.
Netlify, Vercel or Cloudflare Pages
Connect the repository; build command empty, publish directory .. You additionally get
a deploy preview per pull request, instant rollback, and redirect rules.
Any of the four is fine for this site.
After deploying
□ Open the DEPLOYED url, not localhost. Click every link.
□ Open it on a real phone, on mobile data
□ Lighthouse against the live URL, throttled
□ Share the link on WhatsApp — card, or grey box?
□ Visit a URL that does not exist and confirm your 404 page appears
□ Tab through it once more, live
The phone test is the one worth insisting on. Module 4's trick works for local checking, but the deployed site over a real mobile connection is what your visitor gets.
What "done" looks like
The reference site, measured after deployment:
Pages 4, plus a 404
Home page 87.4 KB over 4 requests; 187.8 KB over 8 fully scrolled
Largest transfer hero-2000.webp, 80 KB
Contrast every text element passes AA in light AND dark
Hero text worst composited pixel 5.03:1 at 320px, against a 4.5 requirement
Keyboard 16 stops, all with a ring, order matches the page, no trap
320px no horizontal scroll; the timetable scrolls in its wrapper
400% zoom usable
JavaScript none
Framework none
Build step none for the visitor
Note the gap between 4 requests and 8: lazy loading is saving half the requests and 100 KB for a visitor who does not scroll. That is the measurement that justifies the attribute.
Four pages of HTML and one stylesheet. That is the whole thing, and it is faster than most commercial sites you will visit today.
Keeping it alive
Redeploying is one git push once it is connected.
Check it twice a year. A domain expires, a font disappears, an embed changes its API.
Write the README. In six months it is the only documentation: what the site is, how to run it locally, how it deploys, and where the images came from.
Check your work
When to audit. Before deploying.
The four passes. Keyboard, automated, screen reader, emulations.
What proves there is no keyboard trap. Tabbing all the way round and back out.
What automated tools miss. Roughly two thirds — including whether alt is right and
whether the focus order is sensible.
What the timetable scope check sounds like. "Wednesday, 8:00, Pranayama" rather than
"Pranayama".
Why a flat contrast estimate lies over a photograph. The backdrop is an image, not a background colour — you have to sample the composited pixels.
Why check both colour modes separately. They are not symmetrical; #4b5658 passed by
eye and measured 2.36:1.
Where the honest performance number comes from. The deployed URL, throttled — not localhost.
The most common first-deploy failure. Case-sensitive paths.
Why relative links matter on GitHub Pages. It serves from a subpath, so root-relative links break.
What .nojekyll prevents. GitHub ignoring underscore-prefixed folders.
The test worth insisting on afterwards. A real phone on mobile data.
Practice
- Run all four audit passes on every page of your site. Write down every finding.
- Fix them, then run the keyboard pass again from scratch.
- Run axe with your form in its error state, and compare with the clean-state findings.
- Listen to your timetable — or your hardest content — with a screen reader.
- Measure the contrast of text over your hero image against the composited pixels.
- Measure every field border in both light and dark mode against 3:1.
- Throttle to Slow 4G with 4× CPU and reload. Time it.
- Sort the Network tab by size and justify your largest file.
- Run the full pre-deploy checklist.
- Deliberately rename one file to
About.html, deploy, and watch it 404. Then fix it. - Deploy to GitHub Pages. Confirm every link works on the subpath.
- Deploy the same repository to Netlify or Vercel and compare.
- Open a pull request and find the deploy preview URL.
- Open the deployed site on your phone on mobile data and time it with a stopwatch.
- Share the link on WhatsApp and check the preview card.
- Visit a URL that does not exist and confirm your 404 page appears with navigation.
- Send the link to three people with different phones and ask what looks wrong.
Official documentation
- W3C WAI — Easy checks: a first review — A manual review anybody can run, from the standards body.
- Deque — axe DevTools — The extension, and what each rule checks.
- Chrome DevTools — Lighthouse overview — What each audit measures and how to act on it.
- GitHub Pages — Creating a site — Including
.nojekylland custom domains. - Netlify — Get started — Connecting a repository, previews and rollbacks.
- W3C — How to meet WCAG (Quick Reference) — Filter to AA and use it as the authoritative version of the checklist above.
- The finished site — With its own measurements in the README.
That is the course
You can build a page that works at any width, on any device, for somebody using a keyboard, a screen reader, a magnifier or no colour vision — and you can prove it rather than hoping.
More usefully, you can now read any website and tell why it is the way it is. Inspect the
next site that annoys you and you will find one of the things in this course: a missing
viewport tag, a focus ring somebody removed, an unresized photograph, a <div> pretending
to be a button.
Where to go next. JavaScript Complete Foundation makes these pages do things, and it is far easier now that you know what the DOM is made of. After that, Full-Stack Web Development builds an application with a database and real orders.
And a suggestion: rebuild this capstone for a real business you know — a shop, a clinic, a tutor, your family's business. A live site with a real name on it is worth more in an interview than any certificate, and you now know how to make one that is genuinely good.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship