Here’s a tight, dev-facing read of the EPA homepage you dropped—what’s there, what’s great, and the high-impact fixes.


Snapshot (stack & wiring)

  • Platform: Drupal 10 (core stable components, Views, Paragraphs).
  • Design system: USWDS (banner, nav, grid), custom theme epa_theme.
  • Search: search.epa.gov/epasearch (header drawer; query param querytext).
  • 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 call ga('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 uses aria-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" or aria-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 set fetchpriority="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">
  • Card images: all three are loading="eager". Make only the first above-the-fold eager; mark the others loading="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 uses hero-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; preload
  • Content-Security-Policy: allowlists for scripts/styles/images (Drupal, USWDS, GTM, GA, Touchpoints, search) + upgrade-insecure-requests; set frame-ancestors 'none' or approved origins.
  • Referrer-Policy: no-referrer
  • Permissions-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--rss but 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.js and protected-links.min.js; ensure they add rel="noopener noreferrer" to all target="_blank" anchors (including social).

A quick checklist (highest impact)

  1. Analytics: replace ga() click handlers with dataLayer.push(...) and implement GA4 tags in GTM.
  2. Hero LCP: switch first slide to <img> + fetchpriority="high", or preload the first background image.
  3. Lazy-load non-critical card images (loading="lazy").
  4. Drop legacy jquery.cycle.all.js and the unused RSS/GA legacy snippets.
  5. Security headers + fix allowfullscreen handling (remove if you mean to disable).
  6. Preconnect to GTM/GA/search/touchpoints for faster first use.
  7. 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.