@webgrip/astro-site-toolkit (0.4.0)

Published 2026-09-05 08:32:55 +00:00 by webgrip-ci in webgrip/frontend-toolkit

Installation

@webgrip:registry=
npm install @webgrip/astro-site-toolkit@0.4.0
"@webgrip/astro-site-toolkit": "0.4.0"

About this package

@webgrip/astro-site-toolkit

Build-time guards and rigs shared by Webgrip Astro static sites. Extracted after the two sites' copies of the CSP validator diverged 125 vs 171 lines within a month — copy-reuse demonstrably fails for these; this package is reference-reuse.

Bins

  • webgrip-validate-csp [dist] — asserts every governed inline script AND stylesheet is authorised by its page's own meta CSP, per directive (a style-src hash cannot authorise a script). Wire into the build: "build": "astro build && webgrip-validate-csp".
  • webgrip-serve-dist [dist] — serves dist/ with Cloudflare's auto-trailing-slash semantics and gzip, for Lighthouse rigs that must measure the site, not the server. PORT=0 binds ephemerally; the bound port is printed.

Axe engine

// scripts/axe-scan.ts — the thin per-site file that stays in the consumer:
import { runAxeScan } from '@webgrip/astro-site-toolkit/axe-engine';

await runAxeScan({
  // Pages that matter for a11y but carry no Lighthouse budget — with each
  // site's own argued rationale. This is exactly the part that must NOT be
  // shared.
  a11yOnlyPages: ['/404.html'],
});

Ships TypeScript declarations (lib/axe-engine.d.mts), so a strict consumer needs no shims. Peer deps (consumer installs): @axe-core/playwright, playwright-core. The page list is read from lighthouserc.json so the two gates cannot drift apart.

Mail-auth drift

  • webgrip-validate-mail-auth [intent] (default ops/mail-auth.intent.yml) — reads a per-site intent file that declares, per domain, what should be published, and fails when the world disagrees: MX, SPF, DKIM key length, DMARC policy and report addresses, CAA, DNSSEC, and the MTA-STS policy (DNS records, served as text/plain, byte-identical to the repo copy). Exit 1 on drift, 2 without an intent file. Run it nightly from a scheduled workflow; wire it as "validate:mail-auth": "webgrip-validate-mail-auth".

Two things a generic checker gets wrong are decisions in the intent, not in the engine: a domain whose spf is deliberately-absent fails when an SPF record appears, and a record that is published while the intent still says published: false fails, so the declared state cannot quietly fall behind the world. Steps the intent declares as not yet done are printed, not failed.

# ops/mail-auth.intent.yml
resolver: 8.8.8.8
mtaSts:
  policyInRepo: public/.well-known/mta-sts.txt
  policyUrl: https://mta-sts.example.nl/.well-known/mta-sts.txt
  dnsRecordsPublished: true
domains:
  example.nl:
    mx: [smtp.google.com]
    spf: v=spf1 include:_spf.google.com ~all
    dkim:
      - selector: google
        keyCharacters: 408
    dmarcPolicy: p=none
    dmarcReportsTo: [dmarc@example.nl]
    caa:
      published: true
      issuers: [letsencrypt.org]
    dnssec:
      dsPublished: true

mtaSts is optional; a site without a policy omits it. The engine is importable as @webgrip/astro-site-toolkit/mail-auth (auditMailAuth(intent, lookups)), with lookups injectable so a consumer can test its intent file against a fixture world without touching DNS.

Copy-claims guard

// src/lib/claims.test.ts — the per-site file; the rules are the site's, the engine is shared:
import { bannedCopyViolations, rulesWithoutProof } from '@webgrip/astro-site-toolkit/claims';
import type { ClaimRule } from '@webgrip/astro-site-toolkit/claims';

const FORBIDDEN: ClaimRule[] = [
  {
    name: 'em dash in copy',
    pattern: /—/,
    rationale: 'house style: rewrite the sentence',
    canonical: 'een zin — met kastlijntje',
  },
];

test('source copy carries no banned variants', () => {
  assert.deepEqual(bannedCopyViolations(FORBIDDEN, { roots: ['src/pages'] }), []);
});
test('every rule still matches its own canonical violation', () => {
  assert.deepEqual(rulesWithoutProof(FORBIDDEN), []);
});
  • bannedCopyViolations(rules, options) walks roots (plus extraFiles), keeps files by extensions (default: astro, md, mdx, yml, yaml, html), skips paths matching ignore, and reports file:line [rule] preview with the rationale. By default it scans copy only: Astro frontmatter, <script>/<style>, HTML comments and {expressions} are blanked (line numbers preserved), as are fenced code in Markdown, comments in TypeScript and YAML. A site that keeps copy in frontmatter passes extract: 'raw'. A line containing claims-allow is exempt. A rule with scope applies only to paths that match it; active: false parks a rule.
  • rulesWithoutProof(rules) is the mutation guard: every rule carries a canonical violation, and a rule whose pattern stops matching it is named, so a regex edit cannot silently disarm a rule.
  • retiredVocabularyViolations({ modelPath, exempt }) reads the retired list from a docs/domain/model.yaml (the domain-language skill's format) and sweeps every tracked file for those words, the model and exempt paths excepted. It returns { retired, violations }.
  • missingOgImages({ files, publicDir }) lists ogImage="…" references that point at no file under public/.
  • sourceFiles(options) and copyOf(path, raw) are exported for custom checks.

What stays in the site is exactly what differs per site: the rules, their rationale, the roots and the runtime facts (twente.dev derives its literal-release-fact pattern from the current release entry). Extracted from twente.dev and webgrip.nl, whose two copies had already grown apart in what they blanked and what they reported.

Dependencies

Dependencies

ID Version
yaml 2.9.0

Peer dependencies

ID Version
@axe-core/playwright ^4.10.0
playwright-core ^1.50.0
Details
npm
2026-09-05 08:32:55 +00:00
4
MIT
14 KiB
Assets (1)
Versions (7) View all
0.6.0 2026-09-18
0.5.0 2026-09-05
0.4.0 2026-09-05
0.3.0 2026-09-05
0.2.1 2026-09-04