# Part 5: Accessibility, SEO, and Modern HTML5

## Introduction

Accessibility and SEO are not afterthoughts you bolt on at the end. The structural and semantic choices made in the first four parts of this series — using `<nav>` instead of `<div class="nav">`, writing meaningful alt text, labelling every input — are already accessibility and SEO wins. This part covers the remaining layer: ARIA attributes, meta tags, structured data, and modern HTML5 features that make pages more robust.

I built several static documentation sites and tool pages over time. The improvements in discoverability and usability that came from getting these details right were consistent and measurable.

***

## Accessibility (A11y)

### Why It Matters

Accessibility means building pages that work for people who use keyboard navigation, screen readers (NVDA, JAWS, VoiceOver), voice input, switch access, and other assistive technologies. In practical terms, accessible HTML is also more robust HTML — it works even when CSS or JavaScript fail to load.

Most accessibility in HTML comes from using the right elements. A `<button>` is already focusable, keyboard-operable, and announced as a button by screen readers. A `<div>` with an `onclick` handler requires significant extra work to replicate that behaviour.

***

## ARIA (Accessible Rich Internet Applications)

ARIA attributes extend HTML's accessibility semantics. The first rule of ARIA: **use it only when a native HTML element cannot do the job**.

### `role`

Overrides the element's implicit role when a semantic element cannot be used:

```html
<!-- Avoid: use <button> instead -->
<div role="button" tabindex="0" onclick="doThing()">Click me</div>

<!-- Prefer: native element -->
<button onclick="doThing()">Click me</button>
```

When you genuinely need a custom component:

```html
<div role="tablist" aria-label="Settings tabs">
  <button role="tab" aria-selected="true" aria-controls="panel-general" id="tab-general">
    General
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-security" id="tab-security">
    Security
  </button>
</div>

<div role="tabpanel" id="panel-general" aria-labelledby="tab-general">
  <!-- general settings -->
</div>
```

### `aria-label`

Provides an accessible name when visible text is absent or insufficient:

```html
<!-- Icon-only button -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- X icon --></svg>
</button>

<!-- Multiple navigation elements -->
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Footer navigation">...</nav>
```

Without `aria-label`, a screen reader would announce the navigation elements as "navigation", "navigation", "navigation" with no differentiation.

### `aria-labelledby` and `aria-describedby`

Link elements to labels or descriptions elsewhere in the DOM:

```html
<h2 id="scan-section-heading">Scan Results</h2>
<div role="region" aria-labelledby="scan-section-heading">
  <!-- scan result content -->
</div>
```

```html
<label for="api-key">API Key</label>
<input type="password" id="api-key" name="api_key" aria-describedby="api-key-hint" />
<p id="api-key-hint" class="help-text">
  Your API key is 40 characters and starts with "sk-".
</p>
```

### `aria-hidden`

Hides elements from the accessibility tree (screen readers) while keeping them visible:

```html
<!-- Decorative icon next to text label -->
<button>
  <svg aria-hidden="true" focusable="false"><!-- lock icon --></svg>
  Login
</button>
```

Do not use `aria-hidden="true"` on elements that receive focus.

### `aria-expanded`

Indicates whether a collapsible element is open:

```html
<button aria-expanded="false" aria-controls="dropdown-menu" id="dropdown-toggle">
  Options
</button>
<ul id="dropdown-menu" hidden>
  <li><a href="/profile">Profile</a></li>
  <li><a href="/settings">Settings</a></li>
</ul>
```

JavaScript toggles `aria-expanded` between `"true"` and `"false"` and removes/adds the `hidden` attribute.

### `aria-live`

Announces dynamic content changes to screen readers:

```html
<!-- Status updates that are not critical -->
<div aria-live="polite" aria-atomic="true" id="status-message">
  Scan completed: 7 tests run, 2 bypasses detected.
</div>

<!-- Critical alerts -->
<div aria-live="assertive" role="alert" id="error-banner">
  Connection failed. Check your network and retry.
</div>
```

* `polite` — waits for the user to finish their current action before announcing
* `assertive` — interrupts immediately (use sparingly, only for errors)

***

## Keyboard Navigation

All interactive elements (`<a>`, `<button>`, `<input>`, `<select>`, `<textarea>`) are focusable by default. Tab moves focus forward, Shift+Tab moves backward.

### `tabindex`

| Value                                  | Behaviour                                                          |
| -------------------------------------- | ------------------------------------------------------------------ |
| `tabindex="0"`                         | Element enters the natural tab order                               |
| `tabindex="-1"`                        | Element can receive focus programmatically but is not in tab order |
| `tabindex="1"` (or any positive value) | Avoid — disrupts natural tab order                                 |

```html
<!-- Programmatic focus target (e.g., modal heading) -->
<h2 tabindex="-1" id="modal-title">Confirm Deletion</h2>
```

### Skip Links

Allow keyboard users to jump past repetitive navigation:

```html
<a href="#main-content" class="skip-link">Skip to main content</a>

<nav><!-- long navigation --></nav>

<main id="main-content">
  <!-- primary content -->
</main>
```

```css
.skip-link {
  position: absolute;
  transform: translateY(-100%);
}

.skip-link:focus {
  transform: translateY(0);
}
```

The link is visually hidden until focused by keyboard navigation.

***

## SEO Meta Tags

### Essential Meta Tags

```html
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>simple-waf-scanner: WAF Detection and Bypass Testing Tool</title>

  <meta name="description" content="An open-source CLI tool for WAF detection and bypass testing. Supports 11+ WAF solutions and 280+ payloads with concurrent scanning." />

  <meta name="robots" content="index, follow" />

  <link rel="canonical" href="https://example.com/tools/waf-scanner" />
</head>
```

**Title tag rules:**

* 50–60 characters for search results
* Most important keyword near the start
* Unique per page

**Meta description rules:**

* 150–160 characters
* Accurate summary of the page content
* Not a ranking factor directly, but affects click-through rate

**Canonical URL:** Tells search engines the definitive URL for the page — prevents duplicate content issues when the same page is accessible at multiple URLs (e.g., with/without trailing slash, HTTP/HTTPS).

### Open Graph Tags

```html
<meta property="og:type" content="article" />
<meta property="og:title" content="Building a WAF Scanner in Rust" />
<meta property="og:description" content="How I built a production WAF bypass testing tool using Rust's async capabilities." />
<meta property="og:url" content="https://blog.htunnthuthu.com/building-waf-scanner-rust" />
<meta property="og:image" content="https://blog.htunnthuthu.com/og/waf-scanner.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Htunn's Blog" />
```

The recommended OG image size is 1200×630 pixels. It appears when links are shared on Slack, LinkedIn, Facebook, iMessage, and other platforms that render previews.

### Twitter Card Tags

```html
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Building a WAF Scanner in Rust" />
<meta name="twitter:description" content="How I built a production WAF bypass testing tool." />
<meta name="twitter:image" content="https://blog.htunnthuthu.com/og/waf-scanner.png" />
<meta name="twitter:creator" content="@htunn" />
```

***

## Structured Data (Schema.org)

Structured data provides machine-readable context about page content. Search engines use it to generate rich results (star ratings, FAQ dropdowns, breadcrumbs, etc.) in search results.

Structured data is embedded in a `<script type="application/ld+json">` block:

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Building a WAF Scanner in Rust",
  "author": {
    "@type": "Person",
    "name": "Htunn"
  },
  "datePublished": "2024-03-15",
  "dateModified": "2024-06-01",
  "description": "How I built a production WAF bypass testing tool using Rust.",
  "url": "https://blog.htunnthuthu.com/building-waf-scanner-rust"
}
</script>
```

Common `@type` values:

| Type                  | Use case                           |
| --------------------- | ---------------------------------- |
| `Article`             | Blog posts, documentation articles |
| `BreadcrumbList`      | Navigation breadcrumbs             |
| `FAQPage`             | Pages with Q\&A content            |
| `SoftwareApplication` | Tools and applications             |
| `Person`              | Author profiles                    |
| `WebSite`             | Site-wide metadata                 |

***

## Modern HTML5 Features

### `<details>` and `<summary>`

Native disclosure widget — no JavaScript required:

```html
<details>
  <summary>Advanced Options</summary>
  <div>
    <label for="proxy">Proxy URL</label>
    <input type="url" id="proxy" name="proxy" />

    <label for="user-agent">Custom User-Agent</label>
    <input type="text" id="user-agent" name="user_agent" />
  </div>
</details>
```

Add `open` to make it expanded by default:

```html
<details open>
  <summary>Installation</summary>
  <pre><code>cargo install simple-waf-scanner</code></pre>
</details>
```

### `<dialog>`

Native modal element:

```html
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Deletion</h2>
  <p>This will permanently delete the scan history. This cannot be undone.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>
```

```javascript
const dialog = document.getElementById('confirm-dialog');
dialog.showModal();   // blocks background interaction
dialog.show();        // non-blocking
dialog.close();       // closes with optional return value
```

When opened with `showModal()`, the browser traps focus within the dialog and adds an `::backdrop` pseudo-element for the overlay. This is full modal behaviour natively, without JavaScript libraries.

### `<output>`

Represents the result of a calculation or user action:

```html
<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
  <input type="number" name="a" id="a" value="0" /> +
  <input type="number" name="b" id="b" value="0" /> =
  <output name="result" for="a b">0</output>
</form>
```

### `<progress>` and `<meter>`

```html
<!-- Progress bar: use when there is a defined endpoint -->
<label for="scan-progress">Scan progress</label>
<progress id="scan-progress" value="45" max="100">45%</progress>

<!-- Meter: for measured values within a known range -->
<label for="disk-usage">Disk usage</label>
<meter id="disk-usage" value="0.72" min="0" max="1" low="0.5" high="0.8" optimum="0.3">
  72%
</meter>
```

`<meter>` changes colour automatically based on `low`, `high`, and `optimum` thresholds.

### `<template>`

A client-side content template — not rendered, but available to JavaScript for cloning:

```html
<template id="result-card">
  <article class="result-card">
    <h3 class="result-title"></h3>
    <p class="result-summary"></p>
    <a class="result-link">View details</a>
  </article>
</template>
```

```javascript
const template = document.getElementById('result-card');
const clone = template.content.cloneNode(true);
clone.querySelector('.result-title').textContent = result.name;
document.getElementById('results').appendChild(clone);
```

### `data-*` Attributes

Store custom data on elements without abusing `class` or `id`:

```html
<tr data-scan-id="scan-001" data-waf="cloudflare" data-risk="high">
  <td>example.com</td>
  <td>Cloudflare</td>
  <td>High</td>
</tr>
```

```javascript
const row = document.querySelector('[data-scan-id="scan-001"]');
const waf = row.dataset.waf;      // "cloudflare"
const risk = row.dataset.risk;    // "high"
```

***

## Performance Attributes

### `loading` on `<img>` and `<iframe>`

```html
<img src="below-fold.jpg" alt="..." loading="lazy" />
<iframe src="map.html" loading="lazy"></iframe>
```

### `defer` and `async` on `<script>`

```html
<!-- Download while HTML parses; execute after HTML parsed -->
<script src="app.js" defer></script>

<!-- Download while HTML parses; execute immediately when ready (order not guaranteed) -->
<script src="analytics.js" async></script>
```

Use `defer` for application scripts. Use `async` only for independent scripts (analytics, tracking) that do not depend on other scripts or the DOM.

### `<link rel="preload">`

Tell the browser about critical resources early so it can start fetching them sooner:

```html
<link rel="preload" href="fonts/Inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="hero.jpg" as="image" />
<link rel="preconnect" href="https://api.example.com" />
```

***

## Final Checklist

Before shipping any HTML page:

**Document**

* [ ] `<!DOCTYPE html>` present
* [ ] `<html lang="...">` with correct language
* [ ] `charset="UTF-8"` first in `<head>`
* [ ] `viewport` meta tag present
* [ ] Unique, descriptive `<title>` per page
* [ ] `<meta name="description">` filled in

**Structure**

* [ ] One `<h1>` per page
* [ ] Heading levels not skipped
* [ ] `<main>`, `<nav>`, `<header>`, `<footer>` used appropriately
* [ ] `<div>` not used where a semantic element fits

**Images**

* [ ] Every `<img>` has `alt` text (or `alt=""` if decorative)
* [ ] `width` and `height` specified on all images
* [ ] Off-screen images use `loading="lazy"`

**Forms**

* [ ] Every input has a visible `<label>` linked via `for`/`id`
* [ ] Related inputs grouped with `<fieldset>` / `<legend>`
* [ ] All `<button>` elements have explicit `type`
* [ ] Validation attributes used where applicable

**Accessibility**

* [ ] Page navigable by keyboard alone
* [ ] Skip link present for pages with navigation
* [ ] ARIA attributes only added where native semantics are insufficient
* [ ] No interactive elements with `aria-hidden="true"`

**Performance**

* [ ] Scripts use `defer` or `async`
* [ ] Critical resources preloaded where needed

***

## Summary

| Feature                     | Key Point                                       |
| --------------------------- | ----------------------------------------------- |
| ARIA roles                  | Use only when native elements cannot do the job |
| `aria-label`                | Name elements without visible text              |
| `aria-live`                 | Announce dynamic content to screen readers      |
| Skip links                  | Let keyboard users bypass navigation            |
| Meta description            | 150–160 chars, unique per page                  |
| Canonical URL               | Prevent duplicate content in search indexing    |
| Open Graph tags             | Control social share previews                   |
| Structured data (`ld+json`) | Rich search results                             |
| `<details>` / `<summary>`   | Native disclosure without JavaScript            |
| `<dialog>`                  | Native modal with focus trap and backdrop       |
| `data-*` attributes         | Custom data storage on elements                 |
| `defer` / `async`           | Non-blocking script loading                     |

***

## Series Complete

This series covered the full HTML foundation:

| Part                                                                                        | Topic                                                    |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| [Part 1](https://blog.htunnthuthu.com/getting-started/programming/html-101/html-101-part-1) | Document structure — DOCTYPE, head, body, metadata       |
| [Part 2](https://blog.htunnthuthu.com/getting-started/programming/html-101/html-101-part-2) | Semantic HTML — headings, text content, section elements |
| [Part 3](https://blog.htunnthuthu.com/getting-started/programming/html-101/html-101-part-3) | Links, images, and media                                 |
| [Part 4](https://blog.htunnthuthu.com/getting-started/programming/html-101/html-101-part-4) | Forms and user input                                     |
| [Part 5](https://blog.htunnthuthu.com/getting-started/programming/html-101/html-101-part-5) | Accessibility, SEO, and modern HTML5                     |

From here, CSS handles visual presentation and JavaScript handles behaviour — but both build on the structural and semantic foundation covered in this series.
