RizTech Academy logo
RizTech Academy
Capstone ProjectLesson 4 of 440 min

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

  1. Run all four audit passes on every page of your site. Write down every finding.
  2. Fix them, then run the keyboard pass again from scratch.
  3. Run axe with your form in its error state, and compare with the clean-state findings.
  4. Listen to your timetable — or your hardest content — with a screen reader.
  5. Measure the contrast of text over your hero image against the composited pixels.
  6. Measure every field border in both light and dark mode against 3:1.
  7. Throttle to Slow 4G with 4× CPU and reload. Time it.
  8. Sort the Network tab by size and justify your largest file.
  9. Run the full pre-deploy checklist.
  10. Deliberately rename one file to About.html, deploy, and watch it 404. Then fix it.
  11. Deploy to GitHub Pages. Confirm every link works on the subpath.
  12. Deploy the same repository to Netlify or Vercel and compare.
  13. Open a pull request and find the deploy preview URL.
  14. Open the deployed site on your phone on mobile data and time it with a stopwatch.
  15. Share the link on WhatsApp and check the preview card.
  16. Visit a URL that does not exist and confirm your 404 page appears with navigation.
  17. Send the link to three people with different phones and ask what looks wrong.

Official documentation


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