html: markup validation
html runs every built page through html-validate, an offline HTML validator with rules for content models, duplicate IDs, deprecated attributes and basic accessibility. Invalid markup is where rendering differences between browsers, broken screen reader output and hydration mismatches start. It is not in the default set.
Needs
html-validate9 or later:npm i -D html-validate. Without it the audit ends with!and"html-validate" is not installed. Add it as a dev dependency: npm i -D html-validate.- The build (
dist/). No server or browser is started.
With render.mode on, it checks the DOM a browser renders instead of the shipped HTML file.
Run it
npx vidimus htmlWhat it checks
Every .html file in the build, minus the top-level exclude and html.exclude, is validated with the configured rules. With no pages left the audit is skipped (no built HTML pages).
- Grouping: messages are grouped by rule and message text, so a problem in a shared layout is one finding, not one per page. The finding lists every affected page in
on:and up to three example locations aspath:line:column selector, followed by the rule's documentation URL. When only one page is affected, the finding also names the file (in:). - Message:
<rule-id>: <html-validate message>, for exampleno-dup-id: Duplicate ID "main". Messages that name a specific value (an ID, an element) form separate groups. - Severity: rules html-validate reports as errors fail the audit; rules configured at severity
warn(html-validate severity 1) are warnings. - Fixes: common rules have a specific fix (
no-dup-id,close-order,element-permitted-content,element-permitted-order,attribute-allowed-values,no-deprecated-attr,element-required-attributes,void-style,no-implicit-close,no-raw-characters,wcag/h37); the rest point at the rule's documentation. Every fix ends with how to turn the rule off.
The markup in an <iframe srcdoc="…"> is validated as its own document, usually a fragment, and its example locations read /path/ <iframe srcdoc>:line:column. An <iframe> without title fails element-required-attributes, and invalid sandbox or allow values fail attribute-allowed-values.
Configuration sources
html.extends,['html-validate:standard']by default.- A
.htmlvalidate.jsonin the project root, if present, replaceshtml.extendscompletely: itsextends,rulesand other keys are used as is. Other html-validate config file names (.htmlvalidate.js,.htmlvalidate.cjs) are not read. html.rulesis merged on top of the rules from either source.
The resulting config is marked root: true, so html-validate does not look for further config files next to the built pages.
Example output
─── html ────────────────────────────────────────────────────────
✖ no-dup-id: Duplicate ID "menu"
/:14:9 #menu
/about/:14:9 #menu
/blog/:14:9 #menu
https://html-validate.org/rules/no-dup-id.html
on: / /about/ /blog/ +21 more
→ Give each element a unique id (rename or remove the duplicate), or set html.rules["no-dup-id"] to "off" if it is intended.
✖ element-permitted-content: <div> element is not permitted as content under <p>
/blog/hello/:88:5 article > p > div
https://html-validate.org/rules/element-permitted-content.html
in: dist/blog/hello/index.html
on: /blog/hello/
→ Move the element into a parent that allows it (e.g. no <div> inside <p> or <a> inside <a>), or set html.rules["element-permitted-content"] to "off" if it is intended.
⚠ no-inline-style: Inline style is not allowed
/contact/:40:12 form > div
https://html-validate.org/rules/no-inline-style.html
in: dist/contact/index.html
on: /contact/
→ Fix the markup as described at https://html-validate.org/rules/no-inline-style.html, or set html.rules["no-inline-style"] to "off" if it is intended.
✖ html: 24 pages, 3 distinct problem(s) (1.2s)A clean run ends with ✔ html: 24 pages, valid.
Options
| Key | Default | Description |
|---|---|---|
html.extends | ['html-validate:standard'] | html-validate presets; ignored when .htmlvalidate.json exists |
html.rules | {} | html-validate rules applied on top, e.g. { 'no-inline-style': 'off' } |
html.exclude | [] | URL path patterns to skip |
Presets that ship with html-validate include html-validate:recommended (stricter, adds style rules), html-validate:standard, html-validate:a11y and html-validate:document. See the html-validate presets for what each enables.
import { defineConfig } from 'vidimus';
export default defineConfig({
audits: ['i18n', 'csp', 'a11y', 'links', 'r12s', 'html'],
html: {
extends: ['html-validate:recommended'],
rules: {
'no-inline-style': 'warn',
'no-trailing-whitespace': 'off',
'void-style': ['error', { style: 'omit' }],
},
exclude: ['^/legacy/'],
},
});Rule values follow html-validate: 'off', 'warn', 'error', or [severity, options].
Common fixes
- Duplicate IDs from a component used twice (a menu rendered for mobile and desktop): derive the ID from a prop, or use a class and select with it.
- Block elements inside
<p>: Markdown renderers wrap stray HTML in<p>. Leave a blank line around HTML blocks in Markdown, or use<span>for inline content. void-style: frameworks differ on<br>versus<br/>. Configure the rule to match your output instead of fighting the framework:
html: { rules: { 'void-style': ['error', { style: 'selfclose' }] } }wcag/h37: every<img>needsalt; usealt=""for decorative images.
<img src="/img/divider.svg" alt="" width="600" height="8">- Third-party markup you cannot change (an embedded widget): skip its pages with
html.exclude, or turn the offending rule off. To accept today's problems and fail only on new ones, record a findings baseline.