Contents
1. HTML Page Template
Every page on every site must start from this template. No exceptions. The scoresheet checks for every element listed here — missing any one of them costs you a point.
Head Element Checklist
| Element | Scoresheet Check | Notes |
|---|---|---|
<!DOCTYPE html> | Best Practices: DOCTYPE | Must be first line |
<html lang="en"> | Accessibility: Lang attribute | Required on every page |
<meta charset="UTF-8"> | Best Practices: Charset | Must contain the word "charset" |
<meta name="viewport"> | Accessibility: Viewport meta | Required on every page |
<title> | SEO: Title tag | Unique per page, under 60 chars |
<meta name="description"> | SEO: Meta description | Unique per page, 150-160 chars |
<link rel="canonical"> | Content: Canonical URL | Full absolute URL to this page |
<meta property="og:*"> | SEO: Open Graph tags | At least one og: tag per page |
<link rel="icon"> | Best Practices: Favicon | At least one page (components.js injects it) |
<link rel="preconnect"> | Content: Resource hints | At least one preconnect or preload site-wide |
Body Element Checklist
| Element | Scoresheet Check | Notes |
|---|---|---|
| Skip-to-content link | Accessibility: Skip link | Text must contain "skip" + "main" or "content" |
Semantic wrapper (<main>, <section>, etc.) | Accessibility: Semantic HTML | Every page needs at least one: header, main, nav, footer, section, article |
Exactly one <h1> | Content: H1 + No multiple H1s | Two checks: must exist AND must not be more than one |
| No heading level skips | Accessibility: Heading skips | h1 → h2 → h3 (never h1 → h3) |
| All scripts deferred | Page Weight: No blocking JS | Every <script src> must have async or defer |
| No inline handlers | Best Practices: No inline handlers | Zero onclick=, onchange=, onload=, onerror=, onmouseover= |
| ARIA landmark or role | Accessibility: ARIA | At least one page needs role= or aria-label= (components.js covers this) |
2. 43-Point Scoresheet Reference
The scoresheet (build-process/tools/site-scoresheet.sh) runs 43 checks across 8 categories. Target is A+ (95%+, which means 41/43 or better). Here is every check with the exact pass/fail criteria.
Page Weight & Assets (5 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| Total site weight under 50 MB | HTML + CSS + JS + images < 52,428,800 bytes | Compress images aggressively. Use WebP. |
| No oversized images (>500 KB) | Zero images over 500 KB in /images | Resize and compress before adding to repo. Target 50-200 KB. |
| CSS total under 100 KB | All .css files combined < 102,400 bytes | One stylesheet per site. No frameworks. |
| JS total under 200 KB | All .js files (excluding *-data.js) < 204,800 bytes | Data files are excluded from count. Keep logic JS lean. |
| No render-blocking JavaScript | Zero <script src> tags without async or defer | Always add defer to script tags. |
Performance (5 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| Estimated FCP under 1.8s | Calculated from critical resource sizes | Keep CSS small. Defer all JS. |
| All images have width & height | Every <img> must have both width= AND height= HTML attributes | Use actual pixel dimensions. CSS sizing is not enough — the scoresheet checks HTML attributes. |
| Lazy loading on below-fold images | At least 1 image has loading="lazy" | Add to all images below the fold. Never lazy-load the hero. |
| Font preload enabled | At least one <link rel="preload"> with "fonts" | Preload the primary heading font file. |
| Font display swap | display=swap in Google Fonts URL or @font-face | Google Fonts URLs include this by default: &display=swap |
<img [^>]*width=[^>]*height= to check. The width attribute must appear BEFORE height in the tag. Both must be HTML attributes, not CSS. Get the actual dimensions from the image file and set them explicitly.
SEO (7 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| robots.txt present | File exists at site root | Create during site setup. |
| sitemap.xml present | File exists at site root | Create during site setup. |
| All pages in sitemap | Every HTML file found in sitemap (by filename grep) | See Section 5 for page classification. Private pages need <meta name="robots" content="noindex"> to be excluded from this check. |
| Title tag on all pages | Every HTML file has <title> | Use the page template. |
| Meta description on all pages | Every HTML file has name="description" | Unique per page. |
| Open Graph on all pages | Every HTML file has property="og:" | Use the page template. |
| Structured data (JSON-LD) | At least 1 page has application/ld+json | Add to homepage, product pages, FAQ, contact at minimum. |
index.html) appears in sitemap.xml. If your homepage URL is https://domain.com/ without index.html, it fails the grep. Use https://domain.com/index.html in the sitemap.
Accessibility (8 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| Alt text on all images | Every <img> has alt= attribute | Decorative images use alt="" (empty, not missing). |
| Lang attribute on all pages | Every HTML file has <html lang=> | Use the page template. |
| Viewport meta on all pages | Every HTML file has name="viewport" | Use the page template. |
| Semantic HTML on all pages | Every HTML file contains at least one of: <header>, <main>, <nav>, <footer>, <section>, <article> | Wrap page content in <main>. This is the safest bet — it works even on pages where components.js injects header/footer after parse. |
| No heading level skips | Heading levels increase by 1 (h1→h2→h3) | Never jump from h1 to h3. Plan your heading tree. |
| Skip-to-content link | At least 1 page has text containing "skip" + "main"/"content" | Use the page template skip link. Also checked in JS files. |
| ARIA landmarks / roles | At least 1 page has role= or aria-label= | components.js typically provides this via nav markup. |
| Form labels match inputs | Total <label> elements ≥ total visible inputs | See Section 6. The scoresheet counts <label> tags — NOT aria-label attributes. |
<label> elements in static HTML (script blocks are stripped). It counts <input> (excluding hidden/submit/button/image and tabindex="-1"), <select>, and <textarea>. Labels must be ≥ inputs across ALL pages combined. The aria-label attribute does NOT count. You must have actual <label> elements.
Content Quality (5 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| Every page has an H1 | Every HTML file has at least one <h1> | One per page, no exceptions. |
| No multiple H1s per page | No HTML file has more than one <h1> | Conditional content (blocked states, loading states) must use <h2> or lower. |
| Canonical URL on all pages | Every HTML file has rel="canonical" | Full absolute URL. |
| No generic link text | Zero instances of "click here", "here", "read more", "learn more" as link text | Use descriptive link text: "View our chassis catalog" not "Click here". |
| Resource hints | At least 1 preconnect or preload | Google Fonts preconnect in the template covers this. |
Best Practices (5 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| DOCTYPE on all pages | Every HTML file starts with <!DOCTYPE html> | Use the page template. |
| Charset on all pages | Every HTML file contains "charset" | Use the page template. |
| Favicon linked | At least 1 page has rel="icon" | In template or injected by components.js. |
| Custom 404 page | 404.html exists in site root | Create during site setup. |
| No inline event handlers | Zero onclick=, onmouseover=, onload=, onerror=, onchange= in any HTML file | Use addEventListener in external JS files instead. No exceptions. |
Security (4 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| All links use HTTPS | Zero bare http:// URLs (excludes schema.org, localhost, w3.org) | Always use https://. |
| No exposed email addresses | Zero email-pattern strings on lines without mailto:, "email", placeholder=, or application/ld+json | See Section 7. Always wrap emails in mailto: links. Keep JSON-LD on one line with its script tag. |
| No document.write() | Zero instances in HTML | Never use document.write. |
| No console.log in production JS | Zero console.log in .js files | Remove all debug logging before finalizing. |
Optimization (4 checks)
| Check | Pass Criteria | How to Comply |
|---|---|---|
| WebP alternatives for all JPGs | Every .jpg in /images has a matching .webp | See Section 4 — Image Pipeline. |
| CSS design tokens (>5) | More than 5 CSS custom properties (--variable) | Use the design token system from doc 3. |
| Responsive breakpoints (≥3) | At least 3 @media queries in styles.css | Standard set: 480px, 768px, 1024px minimum. |
| Self-hosted or SRI on external scripts | No external scripts without integrity= | Self-host everything. No CDN scripts without SRI hashes. |
3. Component Architecture
Every site uses a shared pattern: static HTML pages with JavaScript-injected shared components (header, nav, footer). This is the proven architecture from Patriot Chassis and The High Road.
File Structure
How components.js Works
The components.js file runs on every page (loaded with defer). It finds #site-header and #site-footer divs and injects the full header/nav and footer HTML. This ensures:
- Single source of truth — navigation changes happen in one file
- Static HTML pages — no server-side templating needed for layout
- Scoresheet compliance — the injected HTML includes ARIA roles, skip-nav targets, and semantic elements
<div id="site-header"></div> and <div id="site-footer"></div> as injection points. The page's own content must be wrapped in a semantic element (<main>, <section>, etc.) that exists in the static HTML — do not rely on components.js to provide semantic structure for scoresheet compliance.
4. Image Pipeline
Every image added to a site must pass through this pipeline. No shortcuts.
The Pipeline
.jpg file must have a matching .webp with the same filename (e.g., hero.jpg → hero.webp). The scoresheet checks for this. PNG files used for logos/icons are exempt but should have WebP too when possible.php -r 'print_r(getimagesize("path"));' or any image info tool.<img> tag must include: alt, width, height, and (if below-fold) loading="lazy".Image Tag Template
Size Limits
| Asset Type | Budget | Scoresheet Check |
|---|---|---|
| Any single image | < 500 KB | Page Weight: No oversized images |
| All images combined | Part of 50 MB total | Page Weight: Total site weight |
| All CSS combined | < 100 KB | Page Weight: CSS total |
| All JS (non-data) combined | < 200 KB | Page Weight: JS total |
5. Page Classification & Sitemap Rules
Every HTML page falls into one of three categories. The category determines whether it goes in the sitemap and whether it needs a noindex tag.
Category Definitions
| Category | In Sitemap? | Noindex? | Examples |
|---|---|---|---|
| Public content | Yes — listed by filename | No | index.html, about.html, shop.html, gallery.html, faq.html, contact.html, capabilities.html, privacy.html, terms.html, policies.html |
| Private / session | No — excluded by noindex | Yes — <meta name="robots" content="noindex, nofollow"> | login.html, register.html, account.html, cart.html, portal.html |
| System / excluded | No — excluded by script logic | Optional | 404.html, checkout-success.html, checkout-cancel.html, product.html (template) |
404.html, checkout-*.html, quote.html, portal.html, product.html. It also excludes any page with <meta name="robots" content="noindex">. Every other HTML file must appear in sitemap.xml by filename.
Sitemap Format
Noindex Template for Private Pages
6. Form Accessibility Pattern
The scoresheet counts <label> elements and compares to visible form inputs. Labels must be ≥ inputs across all pages combined. This is the most common failure on new builds.
Standard Form Inputs (with visible labels)
Toolbar Controls (visually hidden labels)
Search boxes, sort dropdowns, and filter selects in toolbars don't have visible labels. They still need <label> elements — use the .sr-only class to hide them visually while keeping them accessible and scoresheet-compliant.
Inputs That Don't Need Labels
The scoresheet excludes these input types from the count:
type="hidden"— invisible form datatype="submit"— submit buttons (text is the label)type="button"— custom buttonstype="image"— image buttonstabindex="-1"— honeypot fields (removed from tab order)
<label> element AND an aria-label attribute. The label satisfies the scoresheet; the aria-label provides a better screen reader experience for toolbar controls.
7. Email & Contact Info Handling
The scoresheet flags any email address that appears in HTML or JS source on a line that doesn't contain mailto:, "email", placeholder=, or application/ld+json. It also checks JS files.
Safe Patterns
Unsafe Patterns
mailto: link. Every email in JSON-LD must be on the same line as the application/ld+json script tag. Every email in JS must be on a line containing the string "email" (with quotes).
8. Auth System Pattern
Both Patriot Chassis and The High Road use the same standalone auth pattern. This is the standard for all client-facing sites.
Architecture
| File | Role |
|---|---|
api/auth.php | Login (POST), register (POST), logout (POST). Returns session cookie. |
api/auth-session.php | Session validation (GET). Returns user data if authenticated, 401 if not. |
js/auth.js | Client-side auth state. Checks session on page load, manages login/logout UI, redirects. |
login.html | Login form. Noindex. Redirects to account.html on success. |
register.html | Registration form. Noindex. Redirects to login.html on success. |
account.html | Account dashboard. Noindex. Shows login form if not authenticated, account info if authenticated. |
Session Flow
Security Requirements
- Password hashing:
password_hash($pw, PASSWORD_BCRYPT) - Session cookie: httpOnly, Secure, SameSite=Lax
- CSRF on forms: token in hidden field, verified server-side
- Honeypot field: hidden input with
tabindex="-1"to catch bots - Rate limiting: 5 attempts per IP per 60 seconds on login/register
- Generic errors: "Invalid email or password" — never reveal which was wrong
9. Client Portal Pattern
The client portal is a restricted area for authenticated clients to view their products, orders, and agreements. Both PC and THR use the same dual-state pattern.
Dual-State Page Structure
<h2>, not <h1>. The <h1> belongs to the main portal content. The scoresheet counts ALL <h1> tags in the file regardless of display state. Two H1 tags = failure.
Portal Sections
| Section | Content | Data Source |
|---|---|---|
| Products | Client's product catalog with search, sort, filter | api/client-products.php → site DB |
| Manufacturing | Production status, materials, revision tracking | api/client-products.php with mfg fields |
| Orders | Order history with status tracking | api/orders.php → site DB |
| Agreements | Purchase agreements and terms | api/agreements.php → site DB |
| Account | Profile, company info, password change | api/profile.php → site DB |
10. Product Data & JS Generation
Product data follows the three-tier architecture (see doc 10, section 4). The key implementation detail is how products flow from the database to the frontend.
The Pipeline
generate-products-js.php
This script reads from the site's own DB table (never the Hub) and writes a static JavaScript file that the shop page consumes. Key behaviors:
- Reads from site DB only —
pc_products,ths_products, etc. Neverthr_products. - Outputs
const PRODUCTS = [...]— a JavaScript array of product objects. - Includes variant data — variant families and options are included in each product to avoid N+1 queries on the frontend.
- Excludes non-public products — only products with
is_public = 1appear in the generated file. - File naming convention:
js/products-data.js— excluded from JS size budget by the scoresheet (*-data.jspattern).
Regeneration
After any product sync or deploy, regenerate the JS file:
11. Required CSS Utilities
Every site's styles.css must include these utility classes. They are referenced by the page template, form patterns, and component architecture.
12. Pre-Build Checklist
Before writing any HTML for a new site, complete these setup tasks. They take 10 minutes and prevent hours of rework.
Site Setup (Do First)
css/, js/, api/, images/ (with logo/, hero/, products/, gallery/ subdirs), feeds/, tools/css/styles.css with design tokens from discovery phase, plus the three required utility classes (.sr-only, .skip-link, .hidden-field) and at least 3 @media breakpointsjs/components.js with header, nav, and footer injection. Include ARIA roles, skip-nav target, and semantic HTML in the injected markuprobots.txt — User-agent: * with Sitemap: directivesitemap.xml — start with index.html and add pages as you build them. Use filenames, not bare /404.html — uses the page template, has a friendly message and link back to homepagePer-Page Checklist
For every new HTML page you create:
<main> (or <section> / <article>). Every page must have static semantic HTML.<h1> only. Conditional states (blocked, loading, error) use <h2> or lower.<img> gets alt, width, height. Below-fold images get loading="lazy".<label>. Toolbar controls get <label class="sr-only">.onclick=, onchange=, etc. Use external JS with addEventListener.mailto: links. JSON-LD on one line with its script tag.console.log in production JS. No document.write. No bare http:// URLs.