How a Site Contract Works
Each website can have one active contract, kept as numbered versions with a hash. Every audit stores the hash of the version it applied, so a report always names the rules it was checked against.
- Write the contract on the website's Site contract page, start from the preset, import a file, or accept conventions deduced from your pages.
- Save it: the new version applies from the next audit of each page.
- Read the results in the Site contract group of every report, and in the CLI or GitHub Action output.
Assertion Types
Every assertion has an id (lowercase letters, digits and hyphens), a type, a severity you choose (critical, error, warning or info) and optional parameters. Nothing in a contract runs code: selectors and expressions are data, checked when you save.
| Type | Parameters | Fails when | Source |
|---|---|---|---|
urlPolicy | trailingSlash (never, always, any), lowercase, noQuery, noIndexHtml, maxLength | The final URL breaks one of the rules; the root path is exempt from the slash rule. | Google Search Central (Q39) |
titlePattern | mustMatch, mustNotMatch (regular expressions), minLength, maxLength | The title is missing or breaks a pattern or length. | Google Search Central (S10) |
headOrder | order: charset, viewport, title, description, canonical, og, twitter, icons, preload, stylesheet, jsonld, script | The listed elements present in the head appear in another order. | WHATWG (Q55) |
forbidden | one of selector, htmlComments, jsonLdType | The page has a matching element, an HTML comment, or JSON-LD of that type. | Project decision |
required | selector and count, or min and max | The number of matching elements is outside the range. | Project decision |
headingCase | style (title, sentence), levels, exceptions | A heading of those levels is not written in the style; brands and acronyms keep their spelling. | Project decision |
canonicalPolicy | required, absolute, noQuery, sameHost, selfReferencing | The first canonical breaks one of the rules. | Google Search Central (S12) |
Scope Patterns
An assertion applies to every page unless it has a scope: one URL path pattern or a list of them. * matches inside one path segment and ** across segments, so /blog/* covers /blog/post but not /blog/2026/post, and /docs/** covers every page under /docs/.
Title Case Rules
In Title Case, the first word, the last word, the first word after a colon and every other word are capitalized, except a, an, the, and, but, or, nor, for, so, yet, as, at, by, in, of, on, to, up and vs inside the heading. Both parts of a hyphenated word are capitalized. List brand spellings in exceptions.
Limits and Safety
The limits keep every check fast and predictable, whatever the contract says.
- At most 100 assertions and 64 KB per contract.
- Selectors and regular expressions of at most 300 characters; a selector must parse and an expression must compile.
- An expression that exhausts the backtracking budget on a test string, such as
(a+)+$, is refused when you save. - Regular expressions are written without delimiters and match Unicode text.
The Preset
The preset is a starting contract that most sites can adopt as it is or edit; load it into the editor from the Site contract page.
{
"version": 1,
"assertions": [
{
"id": "url-no-trailing-slash",
"type": "urlPolicy",
"severity": "warning",
"scope": [],
"title": "URLs without a trailing slash",
"trailingSlash": "never",
"lowercase": true,
"noIndexHtml": true
},
{
"id": "title-length",
"type": "titlePattern",
"severity": "warning",
"scope": [],
"title": "Title between 30 and 65 characters",
"minLength": 30,
"maxLength": 65
},
{
"id": "head-order",
"type": "headOrder",
"severity": "info",
"scope": [],
"title": "Head order",
"order": [
"charset",
"viewport",
"title",
"description",
"canonical"
]
},
{
"id": "one-h1",
"type": "required",
"severity": "error",
"scope": [],
"title": "Exactly one h1",
"selector": "h1",
"count": 1
},
{
"id": "one-canonical",
"type": "required",
"severity": "error",
"scope": [],
"title": "Exactly one canonical",
"selector": "link[rel~=\"canonical\"]",
"count": 1
},
{
"id": "no-generator",
"type": "forbidden",
"severity": "info",
"scope": [],
"title": "No generator meta tag",
"selector": "meta[name=\"generator\"]"
},
{
"id": "no-inline-style",
"type": "forbidden",
"severity": "info",
"scope": [],
"title": "No style attributes",
"selector": "[style]"
},
{
"id": "no-html-comments",
"type": "forbidden",
"severity": "info",
"scope": [],
"title": "No HTML comments",
"htmlComments": true
},
{
"id": "no-faq-schema",
"type": "forbidden",
"severity": "warning",
"scope": [],
"title": "No FAQPage structured data",
"jsonLdType": "FAQPage"
},
{
"id": "canonical-clean",
"type": "canonicalPolicy",
"severity": "error",
"scope": [],
"title": "Absolute canonical on the same host, without a query string",
"absolute": true,
"noQuery": true,
"sameHost": true
},
{
"id": "headings-title-case",
"type": "headingCase",
"severity": "warning",
"scope": [],
"title": "Headings in Title Case",
"style": "title",
"levels": [
1,
2,
3
]
}
]
}
Contracts Deduced From Your Site
QuikSEO proposes assertions from the conventions that at least 80% of your audited pages already follow, with the number of pages that follow each one and up to three exceptions. Nothing applies until you accept a proposal and choose its severity; accepted proposals are added to the active contract as a new version.
Use the Contract in CI
Keep quikseo.config.json in your repository and the CLI applies it to every build, so a pull request that breaks a convention fails before it is deployed. quikseo contract pull writes the website's active contract to the file, and quikseo contract push saves the file as a new version.