Skip to content

Quality ​

@templatical/quality is the umbrella package for Templatical's template-quality tooling — deterministic linters that catch authoring mistakes inside the editor and in headless / CI checks. MIT-licensed, ESM, no Vue, no DOM.

Linters ​

LinterWhat it catchesDefault severities
AccessibilityMissing alt text, low contrast, vague CTAs, heading-skip, undersized touch targets, ALL CAPS body, target=_blank missing rel, missing preheader, …mostly error/warning
StructureDuplicate block IDs, sections with the wrong column count, nested sections, empty sections, empty columns, heroes that render as regular sections, background images Outlook on Windows can't layermostly error; some warning
LinksDangerous URL schemes (javascript:), unsupported protocols, malformed mailto: / tel:, relative URLs, staging / localhost URLs leaking into a templatemostly warning; link.javascript-protocol is error
DeliverabilityMismatched URL-shaped link text, shortener links, IP-address links, and image-led emails with little textwarning or info
CompatibilityClient-breaking pass-through HTML and CSS, plus compiled Gmail sizewarning or info
LocalizationMissing translations, stale source hashes and differing merge tagswarning

All linters return the same LintIssue shape and share the same options surface (LintOptions) — so consumers can run them in any combination, merge results, and filter by ruleId prefix (a11y.*, structure.*, link.*, deliverability.*, compat.*, localization.*) when grouping. Each linter's per-rule severities and tool-specific knobs live under its own namespace (accessibility, structure, links, deliverability, compatibility, localization); set any of them to false to disable that linter entirely.

Run everything in one call. lintTemplate(content, options?) is the single aggregate entry point — it runs all linters and returns the merged LintIssue[] (accessibility, then structure, then links, then deliverability, then compatibility, then localization). Reach for it by default; the per-linter functions stay exported for when you want to run just a subset. To skip a category, set its key to false: lintTemplate(content, { structure: false }). See Headless usage.

contentLocale selects the version to lint; locale selects messages. Dictionaries remain a union across languages. The three localization.* rules are listed in Options, with their fallback, staleness and merge-tag behavior.

Architecture ​

TemplateContentJSON block treefrom the editor or DBlintAccessibility()a11y.* ruleslintStructure()structure.* ruleslintLinks()link.* ruleslintDeliverability()deliverability.* ruleslintCompatibility()compat.* ruleslintLocalization()LintIssue[]severity · message · suggestion ·reference · blockId · optional fixConsumed byIssues paneleditor sidebarCanvas badgesper-block iconsHeadless / CIstored templates

The package has no opinion on UI. The editor's useTemplateLint composable lazy-imports @templatical/quality, runs all linters via the single lintTemplate call on debounced content changes, and feeds the merged issues stream into the Issues sidebar tab and the per-block canvas badges. applyFix(issue) runs each patch through the editor's existing block-update path so fixes land as proper undo entries.

Install ​

@templatical/quality is an optional dependency of the editor and installs by default. Leaving it out is supported: the editor works without it and hides the Issues tab. For headless use or to add it back, install it explicitly:

bash
npm install @templatical/quality
bash
pnpm add @templatical/quality
bash
yarn add @templatical/quality
bash
bun add @templatical/quality

CDN users

If you load Templatical via CDN, there's nothing to install. The editor's CDN bundle ships @templatical/quality as a separate code-split chunk that lazy-loads automatically when linting is enabled.

Wire into the editor ​

Pass lint to init() or initCloud():

ts
import { init } from "@templatical/editor";

const editor = init({
  container: "#editor",
  locale: "en",
  lint: {
    accessibility: {
      rules: {
        "a11y.img-missing-alt": "warning",   // soften from default 'error'
        "a11y.text-all-caps": "off",         // turn off entirely
      },
      thresholds: { minFontSize: 16 },
    },
    structure: {
      rules: { "structure.empty-column": "info" }, // demote to info
    },
    links: {
      rules: { "link.localhost-or-staging": "error" }, // promote before send
      nonProductionHosts: ["*.staging.*", "*.preview.*"],
    },
    deliverability: { thresholds: { minTextLength: 250 } },
    compatibility: { thresholds: { maxHtmlBytes: 92_160 } },
  },
});

Open in playground

The Issues tab and inline canvas badges appear automatically once the quality package is available. When lint.disabled === true — or when every per-tool key (accessibility, structure, links, deliverability, compatibility) is false — the editor never lazy-loads the package: no chunk download, no UI surface.

Set any single linter to false to drop just that tool's rules:

ts
init({ container: "#editor", lint: { links: false } });