Skip to content

docs: add style checks for headings and content guidelines#576

Draft
tomarra wants to merge 11 commits into
mainfrom
docs/style-lint-headings
Draft

docs: add style checks for headings and content guidelines#576
tomarra wants to merge 11 commits into
mainfrom
docs/style-lint-headings

Conversation

@tomarra

@tomarra tomarra commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Status

READY (pending #583 merging first — see below)

Description

Adds automated checks for a first draft of content guidelines from marketing, using Vale (a prose style linter) plus a small companion script for things Vale can't see.

What's checked now (npm run lint:content):

  • Headings (##, ###, ...) use sentence case, not Title Case (Shorebird.Headings) — blocking.
  • No exclamation points in prose (Shorebird.Exclamation), ignoring code spans/blocks and markdown image syntax — blocking.
  • <TabItem label="..."> and <LinkButton> text follow the same sentence-case / upper-case rules, via scripts/lint-component-labels.mjs — Vale only parses markdown prose, so it can't see MDX/JSX attributes or isolate component boundaries. Local-only, not wired into CI.
  • "Shorebird", "Flutter", and "Code Push" are always capitalized correctly anywhere in prose, not just headings (Vale.Terms, driven by a Shorebird Vocab) — non-blocking (see below for why).
  • Heuristic check for first-person pronouns (I/we/our/us) in body paragraphs, nudging toward second person (Shorebird.SecondPerson) — non-blocking, content work not started yet.

CI is now blocking (fail_on_error: true) for Shorebird.Headings and Shorebird.Exclamation, which are fully clean after #577, #578, #580, and #583 merge. Vale.Terms and Shorebird.SecondPerson stay at warning severity (Vale's exit code, and therefore fail_on_error, only responds to error-severity alerts) — still reported on every PR, just don't block.

⚠️ Merge-order dependency: do not merge this PR before #583 (heading sentence-case fixes) and #584 (a small regression fix) land. This branch's own CI will show red until then, since its content still has the original ~162 Title Case headings. I verified the full combination (this branch + main + #583 + #584) passes cleanly with fail_on_error: true before pushing this.

Why Vale.Terms stays non-blocking: it doesn't respect code-fence exclusion the way our custom scope:text rules do — it flags literal shell commands inside fenced code blocks (e.g. `shorebird release android`). Worse, a TokenIgnores pattern that successfully suppresses one occurrence of a command intermittently fails to suppress a second, identical occurrence elsewhere in the same file. Confirmed this with several minimal reproductions — it's a genuine Vale bug, not a config mistake. Rather than ship a check that can silently and unpredictably fail on legitimate content, I downgraded it to warning via Vale.Terms = warning. Findings are still visible; they just don't block. Worth revisiting if Vale fixes this upstream.

Known, permanent Shorebird.Headings/Shorebird.Exclamation exceptions (silenced via TokenIgnores, documented in .vale.ini): "vs"/"vs." (Vale flags it unconditionally, regardless of exceptions), compound product names where per-word matching breaks the phrase apart (Azure Key Vault, GCP Cloud KMS, App Store Connect, Firebase Remote Config, Launch Darkly, Hot Reload), three numbered-list headings kept as Title Case for internal consistency, and a table row documenting the literal [!] symbol from real flutter doctor output.

Test plan

tomarra and others added 4 commits July 2, 2026 09:11
Adds a Shorebird.Headings Vale rule that flags Title Case headings and
suggests sentence case, with an exceptions list for brand names,
products, and acronyms. Wires it into CI as a PR-diff-scoped,
non-blocking check (via errata-ai/vale-action) since most existing
headings predate this rule and haven't been migrated yet. Run
`npm run lint:style` locally to check docs content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switches the Vale style-check job to filter_mode: nofilter so it
reports every heading violation on each PR, not just newly touched
ones. Still non-blocking (fail_on_error: false) since this surfaces
the full existing backlog of Title Case headings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Vale-based checks for three more of the marketing team's content
guidelines, alongside the existing heading sentence-case rule:

- Vale.Terms (via a Shorebird Vocab) enforces exact capitalization of
  "Shorebird", "Flutter", and "Code Push" anywhere in prose, not just
  headings.
- Shorebird.Exclamation flags exclamation points in prose, ignoring
  code spans/blocks and markdown image syntax.
- Shorebird.SecondPerson heuristically flags first-person pronouns
  (I/we/our/us) in body paragraphs. Scoped to paragraphs rather than
  headings so it doesn't flag the site's many FAQ-style "Can I...?"
  headings, which are an intentional convention.

Also adds scripts/lint-component-labels.mjs, since Vale (a markdown
prose linter) can't see MDX/JSX attributes or isolate component
boundaries: it checks <TabItem label="..."> for sentence case and
<LinkButton> text for upper case, reusing the same exceptions list as
the heading rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renames lint:style to lint:content and adds cspell as a local
devDependency so all three content checks (spelling, Vale style
rules, and the component-label script) run from a single command
instead of requiring cspell to be run separately via CI's reusable
workflow. Spelling and style stay on separate engines under the
hood (cspell's dictionary is purpose-built for this codebase and
shared via VeryGoodOpenSource/very_good_workflows; Vale's built-in
speller isn't a good fit) but now share one local entry point.

Also fixes a real spell-check CI failure this branch introduced:
"vvago" (from the @vvago/vale package name) wasn't in the cspell
word list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tomarra tomarra marked this pull request as draft July 2, 2026 16:32
@tomarra

tomarra commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Overall this looks good but want to use this to clean things up and then get this to be a blocking check during the PR. Moving to draft for now.

tomarra added 4 commits July 2, 2026 12:17
Adds Stripe, Fastfile, BuildContext, RenderObject, Apple, 1Password,
AOT, GuardSquare, LTS, ExportOptions.plist, Skia, and Impeller to the
Headings rule's exceptions list. These surfaced while applying the
sentence-case fixes in #583 — without them, this check will flag
legitimate proper nouns as false positives once both PRs merge.
Adds TokenIgnores for headings intentionally left as Title Case in
PR #583:
- "vs"/"vs." trips Vale's capitalization rule unconditionally,
  regardless of surrounding case or the exceptions list.
- Compound product/brand names (Azure Key Vault, GCP Cloud KMS, App
  Store Connect, Firebase Remote Config, Launch Darkly, Hot Reload)
  where per-word exception matching breaks the phrase apart.
- Three numbered-list headings kept as Title Case for visual
  consistency with their siblings (see #583 for why).

Without this, these ~9 headings would show as permanent findings
forever, even after all the content PRs merge, making it impossible
to ever flip this check to blocking.
Bumps both custom rules from warning to error severity and flips
fail_on_error to true in CI, so they now actually gate merges instead
of just annotating PRs.

Vale.Terms (proper-noun capitalization) is explicitly downgraded to
warning via `Vale.Terms = warning`, staying non-blocking. It doesn't
respect code-fence exclusion the way our custom scope:text rules do,
and a TokenIgnores pattern matching one occurrence of a shell command
(e.g. `shorebird release android`) intermittently fails to suppress a
second, identical occurrence elsewhere in the same file - confirmed
with minimal reproductions, not a config mistake. Findings are still
reported, just don't block.

Shorebird.SecondPerson stays at warning too, since that guideline's
content work hasn't happened yet.

IMPORTANT: this branch's own CI will fail until #583 (heading
sentence-case fixes) merges - this branch still has the original
Title Case headings. Do not merge this PR before #583. Verified the
full combination (this branch + main + #583 + #584) passes cleanly
with fail_on_error: true.
tomarra added a commit that referenced this pull request Jul 2, 2026
* docs: use sentence case for headers

Per the marketing content guidelines, headers should use sentence
case, not Title Case. Fixes 169 headings across 44 files.

Left several categories of heading untouched, all confirmed by
testing directly against Vale rather than guessing:

- Compound product/brand names where per-word exception matching
  breaks down and partially-casing the phrase reads worse than the
  original: "Azure Key Vault", "GCP Cloud KMS", "App Store Connect",
  "Firebase Remote Config", "Launch Darkly", "Flutter Hot Reload".
- Three "Flutter vs. X" headings: Vale's capitalization rule flags
  any heading containing "vs"/"vs." regardless of surrounding case,
  a hardcoded quirk that isn't configurable via the exceptions list.
- Four "code push" FAQ headings (e.g. "Does code push require the
  internet to work?"): enabling the Vocab-driven proper-noun rule
  causes Vale's capitalization rule to also flag any lowercase vocab
  term (Shorebird/Flutter/Code Push) in a heading, even though sentence
  case itself has no violation here. These already get fixed by the
  proper-noun-capitalization PR; no separate edit needed here.
- Three sequential numbered headings ("1. The Status Enum", "2. The
  Single State Class", "3. The Events"): Vale's algorithm inexplicably
  passes #2 once lowercased but keeps flagging #1 and #3 with
  identical structure. Reverted all three to Title Case to keep the
  numbered list visually consistent rather than partially complying.

Also discovered and fixed real proper nouns that weren't yet
recognized by the Headings rule's exceptions list (Stripe, Fastfile,
BuildContext, RenderObject, Apple, 1Password, AOT, GuardSquare, LTS,
ExportOptions.plist, Skia, Impeller) and two headings using lowercase
"fastlane" where the tool name should be capitalized ("Using Fastlane
on CI/locally"). These exceptions need to land in the tooling PR
(#576) as well, or they'll show as false positives again once these
two PRs both merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix comments

* fix comments

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
tomarra added 3 commits July 2, 2026 14:07
Fixes the two Shorebird.Headings failures found when CI ran against
the whole repo (not just src/content/docs):
- README.md's "# Shorebird Docs" heading
- The Framework Search Paths heading in hybrid-apps/ios.mdx, which a
  review commit deliberately reverted to Title Case as the literal
  Xcode setting name

Also moves Headings-specific exceptions out of .vale.ini's global
TokenIgnores and into Headings.yml's own exceptions list, since that
scopes them to just the Headings rule instead of blunting every rule
for that file. Confirmed the capitalization extension supports exact
multi-word phrase exceptions (e.g. "Azure Key Vault"), not just single
words.

This surfaced two real regressions along the way, both fixed:
- Removing "vs" from the global ignore (now scoped correctly) revealed
  that "Flutter vs. React Native" was never actually resolved - it
  needed "React Native" recognized as its own proper noun.
- Adding common words (Cloud, Reload, Framework) as bare exceptions
  triggers a bidirectional matching behavior in Vale's capitalization
  exceptions: it flags any *lowercase* use of that word elsewhere as
  wrongly-cased, wanting it to match the exception's given casing.
  Confirmed with minimal repros. Reverted those three to phrase-level
  exceptions (Framework Search Paths, GCP Cloud KMS) or, for "Flutter
  Hot Reload" specifically, kept it in .vale.ini's TokenIgnores since
  even the phrase-level exception collides with ordinary "hot reload"
  prose used elsewhere and TokenIgnores' literal-text redaction doesn't
  have this case-insensitive side effect.

Verified 0 errors from `vale --config=.vale.ini .` (matching what CI
actually scans) after this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant