Snapshot (stack & wiring)
- Platform: Drupal 10 (core
stablecomponents, Views, Paragraphs). - Design system: USWDS (banner, nav, grid), custom theme
epa_theme. - Search:
search.epa.gov/epasearch(header drawer; query paramquerytext). - Hero: USWDS “hero slideshow” (custom
hero-slideshow.min.js) with CSS background images. - Content blocks: 3 feature cards; “EPA News Releases” via Views (AJAX capable).
- Analytics: Google Tag Manager (
GTM-L8ZB) + noscript iframe.
Also legacy jQuery handlers that callga('EPA.send', 'event', …)(Universal Analytics syntax). - Fonts: WOFF2 preloads (Source Sans Pro regular/bold/italic + Merriweather bold). 👍
- Extras: Touchpoints feedback widget; external/protected links scripts; jQuery 3.7.1.
A11y & UX
Solid
- USWDS “Official .gov” banner; working skip link to
#main; search toggle usesaria-controls/expanded. - Cards have headings + clear CTAs.
Tune
- Hero images as backgrounds: ensure sufficient text contrast on overlays; background images aren’t read by AT (fine), but confirm hero headings/links are focusable and readable without images.
- Keyboard: confirm the search drawer and mobile menu buttons fire on Enter/Space (USWDS usually covers this—just verify after any theme JS overrides).
- Nav state: add
aria-current="page"oraria-expanded="true"+ visual cue on active section where appropriate.
SEO
Strong, already present
- Canonical, meta description, DC metadata, Open Graph + Twitter (title, description, image). 👍
Minor
<meta name="application-name" content="">— remove or set a value.- Consider
rel="preconnect"to third parties (see Performance) which also helps crawl performance.
Performance
- LCP risk (hero): hero images are CSS backgrounds → no native lazy/priority and no intrinsic sizing. For best LCP:
- Prefer switching hero slides to
<picture>with real<img>so you can setfetchpriority="high"on the first slide and prevent CLS with width/height. - If you must keep CSS backgrounds, preload the first slide:
<link rel="preload" as="image" imagesrcset="/system/files/styles/hero/private/2025-07/20250724_usa-mexico-tijuana-river-mou_hero.jpg 1200w" imagesizes="100vw" href="/system/files/styles/hero/private/2025-07/20250724_usa-mexico-tijuana-river-mou_hero.jpg">
- Prefer switching hero slides to
- Card images: all three are
loading="eager". Make only the first above-the-fold eager; mark the othersloading="lazy"to reduce initial bandwidth. - Resource hints: add
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin> <link rel="preconnect" href="https://www.google-analytics.com" crossorigin> <link rel="preconnect" href="https://touchpoints.app.cloud.gov" crossorigin> <link rel="preconnect" href="https://search.epa.gov" crossorigin> - Legacy plugin: page loads
/sites/all/libraries/cycle/jquery.cycle.all.js(old jQuery Cycle). The hero already useshero-slideshow.min.js; drop Cycle if not actually used. (Also reduces risk surface.) - Aggregation: many CSS includes from
stable/are separate; ensure Drupal CSS/JS aggregation is enabled for fewer requests (unless HTTP/2 multiplexing is measured as fine; still good to aggregate).
Analytics correctness
- Fix legacy UA calls: Theme JS has handlers like:
jQuery(".banner-content a").click(function(){ ga('EPA.send','event','Banner','epahome Click', this.id); });UA (ga()) is sunset and won’t fire unless you’ve shimmed it in GTM. Replace with dataLayer events and map in GTM (GA4):jQuery(".banner-content a").on("click", function(){ window.dataLayer = window.dataLayer || []; dataLayer.push({ event: 'banner_click', link_id: this.id || null, link_url: this.href || null, link_text: jQuery(this).text().trim() }); });Do the same for the other blocks (TopRightGraphic / Panelizer), then build GA4 tags/parameters in GTM. - Event naming: use GA4-style names/params (
event_name,link_text,link_url,section) for consistent reporting.
Security hardening
Serve headers at the edge:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadContent-Security-Policy:allowlists for scripts/styles/images (Drupal, USWDS, GTM, GA, Touchpoints, search) +upgrade-insecure-requests; setframe-ancestors 'none'or approved origins.Referrer-Policy: no-referrerPermissions-Policy: camera=(), microphone=(), geolocation=()X-Content-Type-Options: nosniff
Iframes / fullscreen: your script does
jQuery('iframe').attr('allowfullscreen','0');
Setting allowfullscreen="0" still enables fullscreen (boolean attribute). If the intent is to disable, remove the attribute or sandbox the iframe.
Code & JS cleanup
- Remove dead RSS code path: there’s jQuery fetching
/newsreleases/search/rss/...into.box--rssbut that selector doesn’t exist in this markup. If unused, delete the AJAX block and the Cycle plugin include. - External links: you already load
external-links.min.jsandprotected-links.min.js; ensure they addrel="noopener noreferrer"to alltarget="_blank"anchors (including social).
A quick checklist (highest impact)
- Analytics: replace
ga()click handlers withdataLayer.push(...)and implement GA4 tags in GTM. - Hero LCP: switch first slide to
<img>+fetchpriority="high", or preload the first background image. - Lazy-load non-critical card images (
loading="lazy"). - Drop legacy
jquery.cycle.all.jsand the unused RSS/GA legacy snippets. - Security headers + fix
allowfullscreenhandling (remove if you mean to disable). - Preconnect to GTM/GA/search/touchpoints for faster first use.
- Aggregate CSS/JS in Drupal (if not already) and keep font preloads (those are good).
If you want, I can draft a drop-in <head> patch (preconnect + hero preload) and a tiny replacement JS that swaps all your UA calls to dataLayer.push() in one place.