diff --git a/content/readability.md b/content/readability.md index a54ad12..0fe12e6 100644 --- a/content/readability.md +++ b/content/readability.md @@ -1,6 +1,7 @@ --- title: "Readability" date: 2026-07-17 +modified: 2026-10-24 description: "How ScanGov measures the readability of your content." icon: "fa-solid fa-book-open" category: "product" @@ -11,9 +12,29 @@ videos: title: Readability --- -ScanGov measures how easy a page is to read using two established readability formulas: +ScanGov measures how easy a page is to read with two public formulas: -* [Automated Readability Index (ARI)](https://standards.scangov.org/automated-readability-index) — estimates the U.S. grade level needed to understand a page, based on characters per word and words per sentence. -* [Flesch-Kincaid grade level](https://standards.scangov.org/flesch-kincaid) — a similar grade-level estimate, based on syllables per word and words per sentence. +* [Automated Readability Index (ARI)](https://standards.scangov.org/automated-readability-index) estimates the U.S. grade level a reader needs. It uses letters per word and words per sentence. +* [Flesch–Kincaid readability tests](https://standards.scangov.org/flesch-kincaid) give a similar grade level. They use syllables per word and words per sentence. -Plain language matters for government sites: content written above a general reading level is harder for residents to understand and act on. Readability is its own [indicator](/indicators) with one check, reading level. A page passes when its Automated Readability Index, rounded to the nearest whole number, is at or below 8 (about a 7th grade reading level). ScanGov does not score reading level on pages that are not in English or that have no measurable text, so those pages neither pass nor fail. +Plain language helps people find answers and act on them. Readability is its own [indicator](/indicators) with one check, reading level. + +## How a page passes + +A page passes when its ARI, rounded to a whole number, is at or below your site's reading level target. The default target is grade 8. ScanGov does not score pages that are not in English or that have no text to measure. Those pages neither pass nor fail. + +## Set your target + +Administrators can change the target for each site on the Settings page. + +1. Open the site and select Settings. +2. Under Reading level, choose a grade. +3. Select Save. + +Scores update a few minutes after you save. You do not need to rescan. + +## Review your pages + +Open the Readability indicator, then select All URLs to see every page with its ARI and Flesch–Kincaid score. The ARI card comes first, because ARI decides pass or fail. Both cards show the average over all pages. + +Try the free [readability scan](https://scangov.com/tools/readability/) on any public page. diff --git a/public/css/scangov.css b/public/css/scangov.css index 3b6cdbd..b107722 100644 --- a/public/css/scangov.css +++ b/public/css/scangov.css @@ -498,15 +498,19 @@ button:not(.card *) { } } -/* Skip link — visible on focus for keyboard users (WCAG 2.4.1) */ +/* Skip link — visible on focus for keyboard users (WCAG 2.4.1). Hidden by + Bootstrap's .visually-hidden-focusable until it takes focus; never pair it + with .visually-hidden, which stays clipped on focus. */ .skip-link { position: absolute; - top: -100%; - left: 0; -} - -.skip-link:focus { top: 0; + left: 0; + z-index: 1100; + padding: 0.5rem 1rem; + background-color: var(--bs-body-bg); + color: var(--bs-link-color); + border: 1px solid var(--bs-border-color); + border-top: 0; } /* Navbar */ @@ -1933,6 +1937,19 @@ iframe { border: 1px solid var(--bs-border-color) !important; } +/* Video embeds (lite-youtube-embed, see the youtube shortcode). The + component caps itself at 720px; fill the column like the old iframe did. */ +lite-youtube { + max-width: 100%; +} + +/* The play button covers the whole poster and the component clips paint at + its edge, so the site's offset focus ring would be cut off. Draw it inset. */ +lite-youtube > .lyt-playbtn:focus-visible { + outline: 3px solid #fff; + outline-offset: -5px; +} + /* News posts */ .post-content h2 { @@ -2661,6 +2678,10 @@ td a:hover .fa-circle-info { color: #7efbe1; } +.fa-rotate-left { + color: #7efbe1; +} + .fa-face-frown { color: #fbbaa7; } @@ -2803,6 +2824,8 @@ a .fa-up-right-from-square { [data-bs-theme=light] .fa-circle-nodes:not([class*="text-"]), [data-bs-theme=light] .fa-scale-balanced:not([class*="text-"]), [data-bs-theme=light] .fa-link:not([class*="text-"]), +[data-bs-theme=light] .fa-copy:not([class*="text-"]), +[data-bs-theme=light] .fa-rotate-left:not([class*="text-"]), [data-bs-theme=light] .fa-link-slash:not([class*="text-"]), [data-bs-theme=light] .fa-concierge-bell:not([class*="text-"]), [data-bs-theme=light] .fa-table:not([class*="text-"]), @@ -2860,6 +2883,16 @@ a .fa-up-right-from-square { fill: var(--bs-body-color) !important; } +/* Icons inside a button follow the button's own text color when its background + changes (hover, focus, press). Without this the blanket light-mode icon rule above + keeps them body-colored, so they turn dark on a dark hover background. */ +[data-bs-theme=light] .btn:hover .fa-solid:not([class*="text-"]), +[data-bs-theme=light] .btn:focus-visible .fa-solid:not([class*="text-"]), +[data-bs-theme=light] .btn:active .fa-solid:not([class*="text-"]) { + color: var(--bs-btn-hover-color) !important; + fill: var(--bs-btn-hover-color) !important; +} + [data-bs-theme=light] .bg-secondary-subtle { color: var(--bs-body-color) !important; background-color: #ffffff !important; @@ -3420,6 +3453,7 @@ body[data-bs-theme='dark'] ul.navbar-nav img { .card-footer { color: inherit !important; font-family: var(--bs-font-monospace); + text-align: start; } a.cardlink { diff --git a/scripts/check-sitemap-completeness.js b/scripts/check-sitemap-completeness.js index b89207f..0de35ec 100644 --- a/scripts/check-sitemap-completeness.js +++ b/scripts/check-sitemap-completeness.js @@ -177,6 +177,18 @@ const sitemapPaths = new Set( [...sitemapXml.matchAll(/(.*?)<\/loc>/g)].map((m) => new URL(m[1]).pathname) ); +// The readability pages must stay in the sitemap. The checks below only catch a +// built page that is missing, so a removed or renamed page would go unnoticed. +const REQUIRED_READABILITY_PATHS = [ + '/readability/', +]; +const requiredMissing = REQUIRED_READABILITY_PATHS.filter((p) => !sitemapPaths.has(p)); +if (requiredMissing.length > 0) { + console.error(`check-sitemap-completeness: ${requiredMissing.length} readability page(s) missing from sitemap.xml:\n`); + for (const p of requiredMissing) console.error(` ${p}`); + process.exit(1); +} + let missing, total; try { ({ missing, total } = await checkViaNdjson(siteRoot, sitemapPaths));