Jay-Html Validation Rules Catalog

Design Log #147 — Jay-HTML Validation Rules Catalog

Written for AI agents. See Log Methodology Note below for details.

Background

DL#145 introduced pluggable jay-html validation — a mechanism for plugins to provide validation rules that run against parsed jay-html templates during jay-stack validate. Three validator plugins now exist:

  • wix-media — Wix-specific media optimization rules (external package)
  • seo-validator — SEO best practices (packages/plugins/seo-validator)
  • a11y-validator — WCAG accessibility rules (packages/plugins/a11y-validator)

This design log catalogs all validation rules across all plugins as a single reference.

Validator Plugins

wix-media / media-optimization

Package: @jay-framework/wix-media (external, dev dependency)

Rule Severity Element What it checks
Hardcoded Wix URL without optimization error <img>, <video>, <source> Static URLs from static.wixstatic.com/media/ missing /v1/ transform params
Wix-image binding without optimization error <img>, <video>, <source> Template bindings to tags with meta.mediaType: wix-image without /v1/ suffix appended
Local image reference error <img>, <video>, <source> Local file paths (/images/*.png) that should be uploaded to Wix Media Manager

All three rules check src and poster attributes. Rule B uses walkElements + resolveBinding to resolve bindings through contracts and headless imports.

seo-validator / seo

Package: @jay-framework/seo-validator (monorepo, dev dependency)

Rule Severity Element What it checks
Image missing alt warning <img> No alt attribute (hurts SEO and accessibility)
Image missing dimensions warning <img> No width/height attributes, inline style dimensions, or srcset — causes CLS
Image missing loading warning <img> No loading attribute — should use loading="lazy" or loading="eager"
Empty anchor warning <a> Anchor with href but no visible text, no aria-label, and no child <img>
Missing h1 warning <h1> Page has no <h1> element
Multiple h1 warning <h1> Page has more than one <h1> element
Skipped heading level warning <h2><h6> Heading level skips (e.g., <h1> followed by <h3>)
Missing main landmark warning <main> Page body has no <main> element
Missing fetchpriority warning <img> Page has images but none with fetchpriority="high" — LCP image should be prioritized
Missing title warning <title> No <title> in <head> (via ctx.head)
Missing meta description warning <meta> No <meta name="description"> in <head>
Missing canonical warning <link> No <link rel="canonical"> in <head>
Noindex robots warning <meta> <meta name="robots"> contains noindex

| Missing preconnect | warning | <link> | External stylesheet <link> without a preceding <link rel="preconnect"> for that domain | | Font missing display=swap | warning | <link> | Google Fonts / Typekit stylesheet URL missing display=swap parameter |

The dimensions rule accepts three forms of sizing: width/height attributes, inline style with width: and height:, or srcset (responsive images).

The head metadata rules use ctx.head — a parsed representation of the <head> section added to the validation context.

Render-blocking resource rules (added DL#147a)

Missing preconnect: For each <link rel="stylesheet" href="https://..."> in ctx.head, extract the origin (scheme + host). If no <link rel="preconnect" href="https://that-host"> exists earlier in ctx.head.links, flag it. Only applies to external origins (different from the page's own domain). Suggestion: Add <link rel="preconnect" href="https://fonts.googleapis.com"> before the stylesheet.

Font missing display=swap: Match stylesheet href against known font service domains (fonts.googleapis.com, use.typekit.net). If the URL doesn't contain display=swap (Google) or equivalent, flag it. Suggestion: Add &display=swap to the Google Fonts URL to avoid render-blocking text.

a11y-validator / accessibility

Package: @jay-framework/a11y-validator (monorepo, dev dependency)

Rule Severity Element WCAG What it checks
Image missing alt error <img> 1.1.1 No alt attribute
Form input without label error <input>, <select>, <textarea> 1.3.1 No <label for>, no wrapping <label>, no usable aria-label/aria-labelledby
Empty aria-label error labelable controls 4.1.2 aria-label present but empty/whitespace (DL#166)
Broken aria-labelledby error labelable controls 1.3.1 Empty aria-labelledby or id token(s) missing in file (DL#166)
Duplicate id error any 4.1.1 Same id value used more than once in the file (DL#166)
Button without accessible name error <button> 4.1.2 No text, no aria-label, no aria-labelledby, no child <img alt>
Media autoplay without muted error <video>, <audio> 1.4.2 autoplay attribute present without muted
Invalid ARIA role error any 4.1.2 role attribute value not in WAI-ARIA role list
Viewport disables zoom error <meta> 1.4.4 user-scalable=no or maximum-scale < 2 in viewport meta (via ctx.head)
Nested interactive elements error <a href>, <button>, [role] 4.1.2 A focusable element inside a link/button container (DL#167)
Positive tabindex warning interactive + [role] 2.4.3 tabindex > 0 disrupts natural tab order
Focusable without role warning non-interactive 4.1.2 <div tabindex="0"> or similar without role — screen readers don't know what it is
Multiple controls in <label> warning <label> 1.3.1 More than one labelable control nested in one label (DL#166)
Orphan label[for] warning <label> 1.3.1 for points to an id that does not exist in the file (DL#166)
Adjacent duplicate text warning any Adjacent siblings with identical visible text (screen readers announce twice)

The form label rule skips type="hidden", type="submit", type="button", and type="reset" inputs. Labelable inputs include checkbox and radio (DL#166). Empty aria-label / unresolved aria-labelledby do not count as an accessible name.

The nested interactive rule (DL#167) treats <a href>, <button>, [role="button"], and [role="link"] as containers, and reports one error per focusable descendant — <a href>, <button>, non-hidden <input>, <select>, <textarea>, <summary>, tabindex >= 0, or a widget role.

Rule Overlap

Image alt is checked by both SEO (warning) and a11y (error). This is intentional:

  • Different severity reflects different impact framing
  • A project using both plugins sees both findings — the fix is the same
  • Projects may use only one of the two plugins

No other rules overlap between plugins.

Validation Context: Head Metadata

The JayHtmlValidationContext includes a head?: JayHtmlHeadMeta field parsed from the jay-html <head> section:

interface JayHtmlHeadMeta {
  title?: string;
  meta: Array<{ name?: string; property?: string; content: string }>;
  links: Array<{ rel: string; href: string; [key: string]: string }>;
}

This enables validators to check <title>, <meta>, and <link> tags without needing raw HTML access.

What's NOT statically checkable

These are common accessibility/SEO concerns that can't be validated from jay-html templates:

  • <html lang> — outside jay-html scope (on the <html> element in the page shell)
  • Color contrast — requires computed styles, not available at template level
  • Keyboard traps — requires runtime interaction testing
  • Focus management — requires runtime behavior analysis

Log Methodology Note

Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.