Rules for semantic markup in pcc-foundation-react-app. Good semantics help accessibility, SEO, and maintainability. Pair with WebPerformanceSEO.md for meta tags and Core Web Vitals.
| Element | Where | Rule |
|---|---|---|
<html lang="en"> |
index.html |
Always set lang; add bn spans only inside copy when needed |
<title> + meta |
index.html defaults |
Filled with real home defaults; useSEO updates per route |
<main id="main-content"> |
SiteLayout.tsx |
One main landmark per page — route content only |
| Skip link | SkipToContentLink.tsx |
First focusable element in header; targets #main-content |
| Region | Element | Notes |
|---|---|---|
| Site header | <header> |
TopNavBar — logo + primary nav |
| Primary nav | <nav aria-label="Primary"> |
Desktop NavLinks; mobile #mobile-primary-nav |
| Site footer | <footer> |
Footer — about, quick links, contact, social |
| Footer quick links | <nav aria-labelledby="…"> |
Same routes as top nav |
| Footer social | <nav aria-label="Social media"> |
External links only |
Do not nest <main> inside <header> or <footer>. Do not duplicate primary nav without an accessible name when multiple <nav> exist.
- One
<h1>per page — the page topic (or active hero slide on home). - Do not hide multiple
<h1>in the DOM; render the hero title only on the active slide. - Footer column titles use
<h2>(FooterColumnHeading) — correct depth under the pageh1. - Stub inner pages: single
<h1>matching the route name.
Heading order: h1 → h2 → h3 without skipping levels within a section.
| Pattern | Use |
|---|---|
<article> |
Primary content of a route (about, gallery, event detail) |
<section> |
Thematic blocks inside a page (with aria-labelledby when helpful) |
<aside> |
Tangential content (sidebar teasers — if added later) |
<address> |
Contact details in footer / contact page — not for arbitrary text |
Avoid bare <div> as the only wrapper when a semantic element fits.
Stub pages (current):
<article className="container-site py-12">
<h1>Gallery</h1>
<p>…</p>
</article>The home route is one <article aria-label="Home"> inside <main id="main-content">. Section order matches the live site.
| # | Section | Element | Heading / label |
|---|---|---|---|
| 1 | Hero carousel | <section aria-roledescription="carousel" aria-label="Foundation highlights"> |
<h1> on active slide only |
| 2 | About | <section aria-labelledby="home-about-heading"> |
<h2 id="home-about-heading"> |
| 3 | Partners strip | <section aria-label="Memorial Award partners"> |
No heading — descriptive aria-label |
| 4 | Gallery teaser | <section aria-labelledby="home-gallery-heading"> |
<h2 id="home-gallery-heading"> — “Our Gallery” |
| 5 | Recent events | <section aria-labelledby="home-events-heading"> |
<h2> (when built) |
Gallery teaser grid: <ul aria-label="Foundation event photo highlights"> — each cell is <li> + <Link> to /gallery with descriptive aria-label. Images use meaningful alt from homeGalleryTeaser.ts.
Do not add a second <h1> on home. Footer <h2> columns remain valid under the page h1.
| Element | Rule |
|---|---|
| Section | <section aria-labelledby="home-about-heading"> |
| Heading | <h2> — page h1 stays on the active hero slide only |
| Poster | <figure> + <img> with lazy load; <figcaption> holds PDF download link |
| Read More | <Link> to /our-inspiration — real button, not image-only |
Follow the WAI carousel pattern:
| Piece | Markup |
|---|---|
| Container | <section aria-roledescription="carousel" aria-label="…"> |
| Slide change | aria-live="polite" status text (sr-only) |
| Prev / next | <button type="button" aria-label="Previous slide"> |
| Pagination | <div role="group" aria-label="Slide pagination"> + buttons with aria-current="true" on active dot |
| Decorative overlay | aria-hidden="true" on gradient layers |
| Icons | <i aria-hidden="true"> when a text label exists on the control |
Do not use role="tablist" / role="tab" without tabpanel siblings.
- Navigation →
<a href>or React Router<Link>/<NavLink>. - Actions (menu toggle, carousel, submit) →
<button type="button">. - External links →
rel="noopener noreferrer"+target="_blank". - Phone / email →
<a href="tel:…">/<a href="mailto:…">inside<address>where appropriate.
- Meaningful
alton content images; emptyalt=""only for purely decorative images. - Hero slides: descriptive
altper slide inheroSlides.ts. - Logo link:
aria-labelon the link ifalton the image is sufficient, or rely on imagealtalone — not both redundantly.
Defaults in index.html must be non-empty for title, description, keywords, Open Graph, Twitter, canonical, and geo (Kolkata).
useSEO (in app/hooks/useSEO.ts) syncs per-route:
document.titlemeta[name="title"],description,keywordsog:*andtwitter:*tagslink[rel="canonical"]
When adding a route, update seoPages in useSEO.ts and keep index.html home defaults aligned.
Optional later: og:image:width / height when public/assets/og/og-image.jpg (1200×630) exists.
- Exactly one
<h1>visible per route - Landmarks:
header,nav,main,footerused correctly - Interactive controls are
<button>or<a>, not<div onClick> - Carousels / menus have accessible names and keyboard support
- New route has
useSEOentry + sensible meta copy -
bun run lintandbun run buildpass
- DesignGuidelines.md — brand tokens vs UX liberty
- WebPerformanceSEO.md — LCP, preload, meta / SEO
- FolderStructureRules.md — where components live
- MigrationPlan.md — content and routes