Skip to content

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers - #896

Open
dhananjay6561 wants to merge 22 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs
Open

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers#896
dhananjay6561 wants to merge 22 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs

Conversation

@dhananjay6561

@dhananjay6561 dhananjay6561 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Core Web Vitals + web-quality fixes for the docs site, from the Aug 2026 web-quality action plan (docs tickets D1–D11 and the docs-relevant cross-cutting X/H items).

Docs mobile was failing LCP (~2.6s) mainly on render-blocking web fonts and heavy GIFs. This PR lands every ticket that can be done without a new runtime dependency and without breaking anything — all build-verified. Genuinely-blocked tickets (and why) are listed at the bottom so nothing is silently dropped.

Scope: docs repo only. All landing (L*) and blog (B*) tickets live in their own repos and are out of scope here.


📊 Ticket status

Every docs ticket from the action plan, verified on the served version (v4.0.0CURRENT_DOCS_VERSION; v3.0.0 is not built). File verified on this branch.

ID Area What Key file(s) Status
D1 LCP Self-host DM Sans (variable woff2, latin+latin-ext); drop render-blocking Google Fonts <link> src/fonts/DMSans-*.woff2, src/css/custom.css, docusaurus.config.js ✅ Done
D2 LCP DEAD Delete 6 zero-ref heavy assets (−62 MB) static/{gif,img,cms} ✅ Done
D3 LCP Convert 4 in-use GIFs → H.264 MP4 (−78%); swap 30 refs across 24 files to <video> static/gif, static/img, 24× *.md ✅ Done
D4 LCP Analytics loading refactor: GA + Meta Pixel eager (accuracy — see note), Clarity + Apollo interaction-gated, Hotjar removed src/metaPixelRouteTracker.js, docusaurus.config.js ✅ Done
D5 CLS Stamp intrinsic width/height on raw <img> (dependency-free remark plugin) src/remark/remarkImageSize.js, docusaurus.config.js ✅ Done
D6 LCP Preconnects 6 → 4 (follows from D1) docusaurus.config.js ✅ Done
D9 A11Y aria-hidden on 5 footer social + 12 decorative component SVGs + 6 inline SVGs in the GSoC guide src/theme, src/components, gsoc/contribution-guide.md ✅ Done
D10 DEAD Remove undefined "Aeonik" font-family src/css/custom.css ✅ Done
D11 SEC Client source maps (devtool: source-map, client-only) docusaurus.config.js ✅ Done
X1 PROC CI guard — fail on new image > 500 KB .github/workflows/asset-budget.yml ✅ Done
X3 PROC CI guard — fail on new fonts.googleapis.com/css link .github/workflows/asset-budget.yml ✅ Done
X4 A11Y Accessibility-tree well-formedness for AI crawlers resolved by D9 ✅ Done
H1 SEO robots.txt / sitemap / canonical / JSON-LD present & valid ✅ Verified, no change
H2 SEO Product / review schema ⛔ Won't do — see below

📊 Measured performance (Lighthouse, mobile, median of 9 runs)

Both rows were measured back-to-back on the same machine, so the delta is apples-to-apples. Lab scores are noisy (±10–15 pts run-to-run, and absolute values shift with machine/network/CDN state) — the delta is the reliable signal, not any single number.

Perf LCP TBT CLS
Production (live keploy.io/docs) 61 (53–65) 5.1s ~450ms 0
This PR (shipped) 81 (70–84) 3.1s ~478ms 0

Desktop (shipped): 99 (96–100) / LCP 0.8s / CLS 0.

Net: +20 mobile perf, −2.0s LCP, CLS a perfect 0. Best mobile runs hit 84 / LCP 2.9s; desktop is fully green. Mobile LCP (3.1s) is still above the 2.5s line — the residual is Docusaurus's own React hydration (771 KB) plus the two eager trackers (GA + Meta Pixel), kept eager by product requirement for analytics accuracy.

⚖️ Analytics loading strategy

  • GA + Meta Pixeleager (accurate analytics/conversion from first paint; product requirement).
  • Clarity + Apollo → on first user interaction (engaged sessions only).
  • Hotjarremoved (redundant session-recorder, ~56 KiB).
  • keploy telemetry → eager (first-party, ~2 KiB).

Idle-deferring GA + Pixel too would gain a few more mobile points, but they'd fire ~1–3s after paint — the team chose instant firing. Everything else (fonts, GIF→video, dead-asset removal, image dimensions, a11y, Hotjar removal, Clarity/Apollo gating) is applied.

✅ Changes

Performance — LCP

D1 · Self-host DM Sans
DM Sans loaded via a render-blocking <link rel="stylesheet"> to fonts.googleapis.com in headTags. Now self-hosted as a variable woff2 (latin + latin-ext subsets) in src/fonts/, wired via @font-face in custom.css — mirroring the existing Roboto setup (font-display: swap). It's the same font Google serves modern browsers (DM Sans v17 variable), so letterforms are identical; only the source changes.

D6 · Fewer preconnects
Dropped the two now-unused font preconnects. Preconnects 6 → 4 (algolia, keploy.io, GA, GTM) — clears the ">4 preconnect" warning.

D2 · Delete dead heavy assets (~62 MB)
git rm of 6 assets confirmed 0-ref (grepped repo-wide first):

File Size
static/gif/unit-test.gif 24.8 MB
static/img/unit-test.gif 24.8 MB
static/img/record-testcase.gif 6.0 MB
static/gif/interoperability.gif 2.4 MB
static/gif/tc-generation.gif 1.4 MB
static/cms/reactor.png 2.7 MB

D3 · Convert heavy in-use GIFs to H.264 MP4 (7.44 MB → 1.60 MB, −78%)
The four referenced GIFs over 1 MB were the largest remaining LCP/bandwidth cost. Each was re-encoded to H.264 MP4 with faststart (recipe R3) and the GIF deleted:

Asset GIF MP4 Saved
gif/record-replay 2.66 MB 0.28 MB −89%
gif/replay-tc 2.19 MB 0.41 MB −81%
gif/how-keploy-works 1.45 MB 0.14 MB −90%
img/record-api 1.14 MB 0.76 MB −33%
ffmpeg -y -i in.gif -movflags faststart -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -an out.mp4

All 30 live references across 24 files (v1/v2/v3/v4) were swapped from <img>/![]() to a <video autoPlay loop muted playsInline> element. Each carries explicit width/height (so the browser reserves layout space — a CLS win alongside the LCP one) plus an aria-label describing the clip, preserving the alt text the GIFs had. Autoplaying muted inline video keeps the existing "animated screenshot" behaviour on both desktop and mobile Safari.

static/gif/record-tc.gif (278 KB, 11 references) is intentionally left as a GIF — it's an order of magnitude smaller than the four above, so the conversion churn isn't worth it in this PR. Noted as an optional follow-up.

D4 · Analytics loading
GA and the Meta Pixel fire eagerly — GA via the standard gtag preset (auto SPA tracking), the Pixel via an inline headTags snippet (init + PageView on load, <noscript> fallback intact). This is a product requirement (accurate analytics/conversion from first paint), so they are not deferred — the measured LCP cost is shown in the table above. Clarity + Apollo load on the first user interaction (engaged sessions only), and Hotjar was removed (redundant session-recorder). src/metaPixelRouteTracker.js re-fires the Pixel PageView on SPA route changes and drives the Clarity/Apollo interaction gate.

Performance — CLS

D5 · Intrinsic width/height on raw <img> tags
Docusaurus's mdx-loader already resolves and sizes Markdown images (![](/img/x.png) renders with width/height + a content-hashed asset), but it leaves hand-written HTML <img> tags in .md/.mdx untouched — and those are the remaining source of layout shift. A new dependency-free remark plugin (src/remark/remarkImageSize.js) stamps each raw <img> with the image's intrinsic width/height, giving the browser an aspect ratio to reserve space. The global img { max-width:100%; height:auto } rule keeps images fully responsive — the attributes only supply the ratio, not a fixed size.

Why no dependency: the repo carries both yarn.lock and package-lock.json (Vercel uses yarn, CI uses npm), so image-size/rehype-img-size would mean keeping two lockfiles in sync. The plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic bytes, not extension, so a mislabeled file (this repo has one PNG saved as .jpg) is still sized correctly — verified against sips across all 188 raster assets (188/188 exact match). It never throws, skips remote/relative/data:/webp/svg, and never overwrites author-provided dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author-set widths are left alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx source files change — sizing happens at build time.

Source maps

D11 · Client source maps
A configureWebpack plugin sets devtool: 'source-map' for the client bundle only — no new dependency, emits .map files for debuggable first-party JS, zero runtime impact (428 maps emitted).

Accessibility

D9 · Decorative SVGs (+ X4)
Added aria-hidden="true" to the 5 footer social icons (their <a> already carries aria-label) and 12 decorative component icons. DocItem theme SVGs already had it.

D10 · Remove dead font reference
Removed the "Aeonik" font-family — referenced for headings but never defined via @font-face, so it always fell through to the system stack.

Process / CI

X1 + X3 · Asset & font budget guard
New .github/workflows/asset-budget.yml, scoped to files changed in the PR:

  • X1 — fails on any newly added/modified image > 500 KB.
  • X3 — fails on any newly added render-blocking Google Fonts stylesheet reference (fonts.googleapis.com/css…), enforcing D1.

PR-scoped by design, so pre-existing large assets never fail an unrelated PR — only new regressions are caught.

H1 · SEO sanity — verified robots.txt, sitemap.xml, canonical links, and JSON-LD are all present/valid; no change needed.


🔎 Review feedback addressed

  • CSP has no reporting endpoint — description corrected to state violations are console-only for now; a report-to collector should be wired before enforcing (no fake endpoint added).

🔧 Keeping the linters happy

The D3 <video> markup tripped two checks; both are fixed in this PR:

  • prettier reformatted the markdown HTML block because of the indented <source> child. The two versions disagree on how: 2.8.8 wants a blank line after the opening tag (which would split the JSX block), while 3.9.6 wants the child dedented to column 0. The workflow pins 2.8.8 but the action resolves to 3.9.6, so rather than target either, each <video>/<source>/</video> trio is collapsed onto one line — verified clean under both versions.
  • Vale flagged autoPlay and playsInline as misspellings. They're JSX attribute names, not prose, so they're added to the Base vocabulary alongside the other camelCase identifiers already accepted there (borderRadius, containerName, matchLabels, …).

❌ Not in this PR (and why)

Ticket Priority Why
H2 — Product/review schema P3 SEO Intentionally not done. The "product" half is already covered — the site ships a complete SoftwareApplication JSON-LD block (the correct schema.org type for a dev tool; a separate Product type would be redundant/conflicting). The "review" half is deliberately omitted: aggregateRating/Review markup for one's own product on one's own domain violates Google's structured-data policy (self-serving reviews) and can trigger a manual action suppressing all rich results. Real ratings are surfaced the correct way — via sameAs links to G2/Gartner/Capterra/AWS Marketplace in the Organization schema.
D8 — HSTS response header P2 SEC Preview-only — committed but not effective in prod. vercel.json sets Strict-Transport-Security, but production keploy.io/docs/* is served from S3 + CloudFront, so vercel.json only reaches the Vercel deploy preview. The apex domain already sends the identical HSTS value, so prod is covered; enforcing it on the docs path belongs in the CloudFront config, out of this repo. Kept in vercel.json (harmless, covers the preview).
X6 — COOP (Cross-Origin-Opener-Policy) P2 SEC Same constraint — the same-origin COOP header is in vercel.json and therefore preview-only; real prod enforcement is a CloudFront-config change.
X7 — CSP + Trusted Types (report-only) P2 SEC Same constraint — Content-Security-Policy-Report-Only (incl. require-trusted-types-for 'script') is preview-only and report-only by design (console violations, no enforcement, no report-to collector wired). Enforcing CSP/Trusted Types in prod needs the CloudFront config plus a reporting endpoint.

✅ Verification

  • npm run build[SUCCESS]; onBrokenLinks: "throw" passes — but that validates links, not <img>/<video>/<source> src. That the D2 deletions and D3 swaps break zero references was verified by grep, and is now enforced by the asset-budget guard (new step: every added src="/docs/…" must resolve under static/).
  • CI: Vale, prettier, asset-budget, run-lint, deploy-preview green. DCO pending — sign-off still needed on a few commits.
  • Fonts: both woff2 subsets emitted + referenced by built CSS (no broken URL); no Google Fonts stylesheet; no gstatic/googleapis preconnect; 4 preconnects total.
  • D3: all 4 GIFs deleted with zero remaining live references (the only mentions left are inside non-rendering [//]: # markdown comments that predate this PR); 30 <video> elements across 24 files, each with width, height, and aria-label; MP4s total 1.53 MiB.
  • D5: remark parser output matches sips on 188/188 raster assets; built HTML shows 30 raw <img> with injected width/height, 69 author-sized tags untouched, Markdown images unaffected, 0 duplicate attributes; disabling the plugin drops the attributes (isolation confirmed).
  • D4: GA eager (gtag preset); Meta Pixel eager (inline headTags snippet, init + PageView, <noscript> intact); Clarity + Apollo load on first interaction; Hotjar removed; SPA PageView re-fired from the client module.
  • D9 / X4: 5 footer + 12 component SVGs carry aria-hidden (all 16 <svg> in src/ covered, per-tag verified) plus the 6 inline decorative SVGs in gsoc/contribution-guide.md — the one served-markdown gap the earlier sweep missed.
  • D10: no real Aeonik font-family remains.
  • D11: 428 .js.map files emitted.
  • X1/X3: workflow valid YAML; simulated on this PR's diff → passes; on a real violation → fails as intended. Image budget now uses --diff-filter=AMR so a renamed+re-encoded file can't slip it; added a step that fails on an added src="/docs/…" not resolving under static/.
  • H1: robots.txt, sitemap.xml, canonical, JSON-LD all present/valid.
  • prettier --check clean on all changed files under both 2.8.8 and 3.9.6; Vale clean on changed lines; no yarn.lock churn.

Local preview: npm run serve (production preview) can't serve the fonts due to the repo's trailingSlash: true setting (it 302→404s every woff2 — the pre-existing Roboto font too), so DM Sans falls back to a system font locally. Use npm start (dev server) to preview fonts correctly. On Vercel the self-hosted fonts load fine, exactly like Roboto does today.


🔄 Review iteration 5 — fixes pushed

  • D9 / X4 gap closed — the 6 inline decorative GitHub-icon <svg> in
    gsoc/contribution-guide.md (a served v4 page) now carry
    aria-hidden="true" + focusable="false". That was the only served-markdown
    SVG the earlier component/footer sweep missed, so X4 is now genuinely
    complete.
  • X1 rename gap — the image-budget check used --diff-filter=AM, so a
    git mv that re-encoded a file larger reported as R and was skipped
    entirely. Now --diff-filter=AMR.
  • Asset-src resolution now guarded — added an asset-budget step that fails
    when an added src="/docs/…" in changed markup doesn't resolve to a file
    under static/. onBrokenLinks only validates links, so this makes the
    D2/D3 "breaks zero references" claim self-enforcing (and the verification
    line is corrected to stop attributing that proof to the build).
  • D8 / X6 / X7 documented, not dropped — moved into ❌ Not in this PR with
    the real reason: those headers (HSTS, COOP, CSP-Report-Only) live in
    vercel.json, which only reaches the Vercel deploy preview — production
    /docs/* is served from S3 + CloudFront, and the apex already sends the same
    HSTS. Prod enforcement belongs in the CloudFront config.

Deferred (non-blocking, called out honestly): concepts/what-are-keploy-features.md
is the only v4 page with two eager autoplay <video>s (~742 KB fetched on load
regardless of viewport). It's the tail of D3, not systemic; a proper fix is an
IntersectionObserver that assigns src to the below-fold video. Left for a
follow-up rather than adding a one-page lazy-video component to this PR. Same for
prefers-reduced-motion on the autoplay set.

Copilot AI lite review requested due to automatic review settings August 7, 2026 05:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the docs site’s Core Web Vitals (notably LCP) and security posture by removing render-blocking third-party resources, self-hosting fonts, deferring non-critical analytics, and tightening response headers. It also includes small accessibility fixes for decorative SVGs.

Changes:

  • Self-host DM Sans (woff2 variable font subsets) and remove Google Fonts preconnect/stylesheet from headTags.
  • Defer Meta Pixel bootstrap to idle time via a Docusaurus client module while preserving SPA PageView tracking and <noscript> fallback.
  • Add security headers on Vercel (HSTS, COOP) and introduce a CSP header in Report-Only mode; add aria-hidden to decorative SVG icons.

Reviewed changes

Copilot reviewed 9 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vercel.json Adds HSTS, COOP, and CSP Report-Only header configuration.
src/metaPixelRouteTracker.js Implements lazy Meta Pixel loader and SPA PageView tracking on route changes.
src/css/custom.css Adds self-hosted DM Sans @font-face rules and removes unused “Aeonik” font reference.
src/components/WhatIsKeploy.js Marks decorative SVGs as aria-hidden and applies formatting tweaks.
src/components/UtgMethods.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Resources.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Product.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Intro.js Adds aria-hidden="true" to decorative SVG icons.
docusaurus.config.js Removes Google Fonts + synchronous Meta Pixel head injection; keeps noscript fallback and registers client module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vercel.json
Comment thread src/metaPixelRouteTracker.js Outdated

@dhananjay6561 dhananjay6561 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review — perf/web-vitals-docs

Reviewed following the four-phase process (context → high-level → line-by-line → summary). This is a well-scoped, well-documented PR: the ticket table maps every change to a rationale, the remark plugin is genuinely careful (magic-byte parsing, never-throws, path-traversal guard, author-dimension preservation), and the risky bits (CSP, source maps) are correctly landed in non-enforcing / no-runtime-impact modes. Build + CI are green and the description is honest about what's deferred and why.

No blocking issues. I left a handful of inline notes — one 🟡 (a real but narrow analytics gap in the pixel loader whose code comment overstates what happens), plus a few 🟢/💡 hardening nits on the plugin cache and the CI guard. None need to block merge; the pixel one is worth a quick look.

🎉 The remarkImageSize plugin is the standout — choosing the parser by magic bytes rather than extension (and verifying 188/188 against sips) is exactly the right call for a repo with a mislabeled asset.

Comment thread src/metaPixelRouteTracker.js Outdated
Comment thread src/remark/remarkImageSize.js
Comment thread .github/workflows/asset-budget.yml Outdated
Comment thread .github/workflows/asset-budget.yml Outdated

@dhananjay6561 dhananjay6561 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — all four notes resolved ✅

Re-reviewed the three follow-up commits (5e32dce, 256c6a6, 3f09246) against my earlier comments. Each is fixed correctly:

  • 🟡 Dropped PageView (5e32dce) — first real SPA navigation now calls bootstrapPixel() synchronously instead of re-scheduling into the not-yet-fired idle window. That creates the fbq stub and fires init + this page's PageView (queued until fbevents.js loads), so the navigation is no longer lost, and any already-scheduled idle callback cleanly no-ops via the window.fbq guard. The corrected comment now matches the behavior. Resolved.
  • 🟢 sizeCache (256c6a6) — per-process caveat documented (build = fresh process/accurate; dev server = restart to pick up resized assets). Resolved.
  • 🟢 Redundant shallow fetch (3f09246) — dropped, with a comment noting fetch-depth: 0 already provides the ancestry the three-dot merge-base needs. Resolved.
  • 💡 AVIF budget gap (3f09246) — avif added to the image regex, so the guard now matches the format its own error message recommends. Resolved.

No new issues introduced. Nothing outstanding from my side — LGTM. 🎉

@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 3

Scope: the 4 commits landed after Iteration 2 (34a6383, 814db2d, 9d68f6b, 2295ef4) plus a full re-pass over the whole diff (57 files, +537 / -112). Four-phase process: context, high level, line by line, summary.

VERDICT: 🔄 REQUEST CHANGES (1 blocking). No code here is wrong. The blocker is that 3 tickets marked ✅ Done ship nothing to real users.

Tally: 🔴 1 · 🟡 4 · 🟢 4 · 💡 2 · 📚 1 · 🎉 5


🔴 BLOCKING (1)

🔴 D8 / X6 / X7 never reach production. keploy.io/docs/* is served from S3, not Vercel.

Measured on live prod:

$ curl -sI https://keploy.io/docs/keploy-explained/introduction/
HTTP/2 200
server: AmazonS3
via: 1.1 ... (CloudFront)
cache-control: max-age=public,

No x-frame-options, no x-content-type-options, no referrer-policy, no permissions-policy. Those four are already in vercel.json on main and are absent in prod, which proves vercel.json is not the serving path. .github/workflows/main.yml:45-56 builds and pushes to S3 via reggionick/s3-deploy plus a CloudFront invalidation. vercel.json governs the deploy preview only.

So HSTS, COOP, CSP-Report-Only and Trusted Types land nowhere for real users.

Two related facts:

  • The apex already sends the identical HSTS value, and HSTS is per host, so /docs is covered:
    strict-transport-security: max-age=63072000; includeSubDomains; preload on https://keploy.io/. The HSTS line in this PR is redundant even if it did ship.
  • The apex enforces a full CSP (with worker-src, form-action, media-src). Docs pages get no CSP at all. That is the real gap, and vercel.json does not close it.

Fix: deliver these via a CloudFront response headers policy (or S3 object metadata in the deploy step), then re-label D8 / X6 / X7 in the ticket table. Keeping vercel.json for previews is fine, just do not mark them done.


🟡 IMPORTANT (4)

🟡 1. Cache-Control on prod is malformed, so D1 and D3 lose their repeat-visit win.
main.yml:54 sets cache: "public, max-age:86400", a colon instead of =. Prod serves cache-control: max-age=public,, an invalid delta-seconds, so nothing under static/ gets a valid freshness lifetime. The new woff2 pair (54 KB), 4 MP4s (1.53 MB) and 4 posters (228 KB) all inherit it. Google Fonts previously served DM Sans with a long immutable cache, so self-hosting is a repeat-visit regression until this is fixed. One character, plus ideally a max-age=31536000, immutable rule for /docs/fonts/.

🟡 2. Stale comment contradicts the shipped strategy.
docusaurus.config.js:641-646 says GA + Meta Pixel -> on idle and "loaded from src/metaPixelRouteTracker.js instead". Shipped state: GA via the gtag preset, Pixel via the headTags inline snippet, both eager. Only Clarity and Apollo moved into the client module. This is the third place the strategy is written down and the only one that is wrong.

🟡 3. mousemove defeats the Clarity/Apollo interaction gate on desktop.
src/metaPixelRouteTracker.js:24. Any desktop visitor moves the pointer within milliseconds of paint, so both scripts load almost immediately. "Engaged sessions only" holds for touchstart and scroll, not for mousemove. Drop it, or put it behind a movement threshold.

🟡 4. 12 of 30 <video> tags hardcode https://keploy.io.
All in the v1 to v4 glossary files, for both poster and <source>. Inherited from the old <img src>, but it matters more now: the deploy preview and localhost pull production media, so the new MP4s are not verifiable in the preview for 12 of 30 spots. The docs CSP also declares no media-src, so it falls back to default-src 'self', which passes on the prod origin by accident and would block anywhere else once enforced. Make them relative /docs/... like the other 18.


🟢 NITS (4)

  • No pause control on 30 autoplay looping videos. WCAG 2.2.2. Not a regression (GIFs were worse), but <video> makes it fixable: gate autoplay on prefers-reduced-motion in JS, since CSS cannot stop autoplay.
  • record-api.mp4 is 744 KB, 2.7x the next largest, and oversized. 1074x782 intrinsic, rendered at 80% of a ~750px column. Its poster is 154 KB, also the largest of the four. Downscaling to ~800px wide cuts both.
  • X3 font guard misses .css. asset-budget.yml:49 pathspec has no *.css, so @import url(https://fonts.googleapis.com/css2...) in custom.css slips past. That is exactly the mechanism the old code comment referenced.
  • Docs CSP diverges from the apex policy. Missing form-action and media-src (apex has both), and script-src 'unsafe-inline' with no nonce buys little XSS protection while require-trusted-types-for 'script' is declared. Two divergent policies on one origin is a maintenance trap.

💡 SUGGESTIONS (2)

  • Follow the Roboto precedent instead of an inline <style>. custom.css:206-228 already does @font-face { src: url("../fonts/Roboto-Light.woff2") } from src/fonts/, webpack-processed and content-hashed. The new comment claims css-loader "refuses to resolve" the URL from src/css, but Roboto proves relative paths resolve fine there. That route keeps @font-face in CSS, drops the inline <style> (which needs style-src 'unsafe-inline'), and gets content hashing plus immutable caching for free, which also sidesteps 🟡1. Minor: the PR table lists src/fonts/DMSans-*.woff2, the files actually landed in static/fonts/.
  • D2 missed dead JS while sweeping dead images. static/js/code-block-buttons.js has zero references. static/scripts/fullstory.js and chat.js are referenced only from the commented block at docusaurus.config.js:654-668. Same ticket, one more pass.

📚 LEARNING (1)

remarkImageSize.resolveStaticPath skips paths starting with . as relative, but a bare img/x.png (relative, no dot) is treated as static-rooted, so it could stamp another file's dimensions if a same-named file exists under static/. Zero such tags in the repo today, so latent only.


🎉 PRAISE (verified, not taken on trust)

  • Magic-byte parser selection plus the 188/188 sips cross-check. Right call for a repo with a mislabeled asset.
  • All 4 poster/MP4 pairs match exactly, checked with ffprobe: 800x348, 800x450, 562x356, 1074x782. Every declared width/height matches the real stream, so the reserved space is correct. Posters are paired to the right source in all 30 tags.
  • img, video { max-width: 100%; height: auto } does exist at custom.css:933-937, so the injected attributes supply ratio without fixing size. Claim holds.
  • Zero live references to the 10 deleted assets. The 3 remaining mentions sit inside non-rendering [//]: # comments, exactly as stated.
  • Honest "Not in this PR" section, and the call on self-serving review schema is correct.

Verify before merge

  1. Confirm where response headers for keploy.io/docs/* actually originate, then move D8 / X6 / X7 there.
  2. curl -I the woff2 on the deploy preview and check both the 200 and the Cache-Control.
  3. versioned_docs/version-3.0.0 is excluded by onlyIncludeVersions (docusaurus.config.js:461), so the 5 v3 files edited here are never compiled. Not build-verified.

Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 4

Scope: commit 944bbd33 ("address review iteration 3"), re-verified against every finding from Iteration 3. Diff vs the previously reviewed head: 23 files, +19 / -136.

VERDICT: 💬 COMMENT. Code is ready to merge. Eight of nine findings are fixed correctly and I confirmed each one rather than taking the commit message for it. One item is left: it is a description fix, not a code fix.

Tally: 🔴 0 · 🟡 1 (description only) · 🟢 2 open · ✅ 8 fixed


✅ FIXED AND VERIFIED (8)

Iter-3 finding Fix Verified how
🟡 Malformed Cache-Control main.yml:54 now public, max-age=86400 = replaces :
🟡 Stale analytics comment Rewritten to name the real mechanism per tracker docusaurus.config.js:641-647 matches the shipped config
🟡 mousemove defeats the gate Dropped, with a comment saying why INTERACTION_EVENTS is now 4 events
🟡 12 hardcoded https://keploy.io videos All 12 relative 0 absolute URLs left in any <video> tag
🟢 record-api.mp4 oversized 744 KB → 318 KB (-57%), poster 154 KB → 56 KB (-63%) ffprobe: stream is 800x582, poster is 800x582, declared width/height is 800x582. All three agree
🟢 X3 guard misses .css *.css added to the pathspec asset-budget.yml:49
📚 Bare relative <img src> Now requires a leading /, so img/x.png is skipped resolveStaticPath. Stricter than I suggested, and correct
💡 Dead JS missed by D2 code-block-buttons.js, chat.js, fullstory.js deleted (125 lines) plus the commented config block file list

CI is green on all 7 checks.


🟡 STILL OPEN (1, description only)

🟡 D8 / X6 / X7 were deleted from the ticket table instead of being re-labeled.

The rows are gone from the status table, but:

  • vercel.json still ships HSTS + COOP + CSP-Report-Only in this PR.
  • The three tickets appear nowhere in "❌ Not in this PR (and why)".

The PR summary says: "Genuinely-blocked tickets (and why) are listed at the bottom so nothing is silently dropped." These are now silently dropped, which is the one outcome that sentence rules out. Deleting the row also loses the finding: the next person to pick up D8 has no record that vercel.json cannot deliver it.

Fix: move D8 / X6 / X7 into the "Not in this PR" table with the reason (production /docs/* is served from S3 + CloudFront, so vercel.json only reaches the deploy preview), and note that the apex already sends the identical HSTS value. Keeping the vercel.json block is fine; it just needs to be described as preview-only.


🟢 NOT ADDRESSED, FINE TO DEFER (2)

  • CSP has no form-action or media-src, and script-src 'unsafe-inline' has no nonce. Unchanged. Only bites when the header is actually delivered, so it belongs with the CloudFront work above rather than here.
  • No prefers-reduced-motion handling for the 30 autoplay looping videos. Unchanged. Not a regression against the GIFs, and needs JS since CSS cannot stop autoplay. Reasonable as a follow-up.

The 💡 about moving the font to src/fonts/ with the Roboto relative-url pattern is worth much less now that the cache header is fixed. Dropping it is the right call.


🎉 PRAISE

  • Every fix was verified rather than asserted, and the two I expected to be fudged were not: the record-api re-encode updated the declared width/height and regenerated the poster at the new size, so the aspect ratio stays exact.
  • The resolveStaticPath fix is stricter than the finding asked for. Requiring a leading / closes the whole class instead of the one case I named.
  • The mousemove removal carries a comment explaining the reasoning, so it will not get re-added by someone adding "more interaction signals".

Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 5

Scope: no new commits since Iteration 4 (head is still 944bbd33), so this is a deeper pass over ground the earlier iterations did not cover: the D9/X4 sweep beyond JSX, the X1 guard's filter semantics, what the build actually validates, and the remaining eager media cost.

VERDICT: 💬 COMMENT. Two things before merge, neither is new code. One is the Iteration 3 blocker still unresolved, one is a new gap in D9/X4.

Tally: 🔴 1 carried · 🟡 1 new · 🟢 3 new · ✅ 2 risks ruled out


🟡 NEW: D9 / X4 stop at JSX and miss 6 SVGs in served markdown

All 16 <svg> in src/ carry aria-hidden, aria-label, or role. Clean. But versioned_docs/version-4.0.0/gsoc/contribution-guide.md has 6 inline decorative <svg> with none of the three, so they are still exposed to the accessibility tree on a served v4 page.

D9 reads "aria-hidden on 5 footer social + 12 decorative component SVGs ✅ Done" and X4 ("accessibility-tree well-formedness for AI crawlers") is marked resolved by D9. The sweep was scoped to components and the footer, not to the version. Either add aria-hidden="true" to those 6 or mark X4 partial.


🟢 NEW (3)

🟢 1. X1 misses renames. asset-budget.yml:40 uses --diff-filter=AM. Rename detection is on by default in git diff, so a renamed file reports as R and is skipped entirely. git mv static/big.png static/img/big.png while re-encoding it larger slips the 500 KB budget. Use --diff-filter=AMR, or drop the filter and let the existing [ -f "$f" ] || continue handle deletions.

🟢 2. The verification line misattributes its own evidence. The PR says "npm run build[SUCCESS]; onBrokenLinks: "throw" passes (the D2 deletions and D3 swaps break zero references)". Docusaurus's broken-link checker validates links, not <img> / <video> / <source> src attributes, so the build cannot prove that claim. The grep did. I checked the conclusion separately and it holds: nothing in src/ builds an asset path by concatenation or template literal, so there is no dynamic reference the grep could have missed. The claim is correct, the stated proof is not, and nothing guards it going forward. Worth extending asset-budget.yml with a check that every src="/docs/…" in changed markdown resolves to a file under static/. That makes the claim self-enforcing and costs about ten lines.

🟢 3. One page still ships 742 KB of eager autoplay media. concepts/what-are-keploy-features.md carries 2 videos plus posters. Autoplay forces the fetch, and there is no lazy equivalent for <video>, so both download on load regardless of viewport. It is the only 2-video page in v4, so this is the tail end of D3 rather than a systemic issue: an IntersectionObserver that assigns src to the below-fold one would clear it.


✅ RISKS CHECKED AND RULED OUT (2)

  • EXIF orientation in remarkImageSize. A JPEG with orientation 5 to 8 displays with swapped dimensions, so stamping the raw SOF values would produce an inverted aspect ratio and cause layout shift instead of fixing it. Their sips cross-check would not have caught it either, since sips reports stored dimensions too. Checked all 13 JPEGs under static/: 0 rotated. Not an issue, now or by accident.
  • D9 coverage in components. 16 of 16 <svg> in src/ are covered, and the footer SVGs in docusaurus.config.js all carry aria-hidden plus focusable="false". The only gap is the markdown one above.

🔴 CARRIED FROM ITERATION 3, STILL OPEN

D8 / X6 / X7 were deleted from the ticket table rather than re-labeled. vercel.json still ships HSTS + COOP + CSP-Report-Only, the three tickets appear nowhere in "❌ Not in this PR (and why)", and the summary still promises that blocked tickets are listed there so nothing is silently dropped. Move them into that table with the reason: production /docs/* is served from S3 + CloudFront, so vercel.json only reaches the deploy preview, and the apex already sends the identical HSTS value.

Also still open and fine to defer: CSP has no form-action or media-src, and there is no prefers-reduced-motion handling for the 30 autoplay videos.


🎉 PRAISE

The magic-byte parser choice turns out to be load-bearing in the narrowest possible way. The only raw <img> in v4 with a .jpg extension is /docs/img/keploy-test-openhospital.jpg, and that file is the mislabeled PNG. An extension-based parser would have handed it to the JPEG reader, failed the SOI check, and silently skipped the one JPEG-named image the plugin actually has to size. Choosing by magic bytes was not defensive programming for a hypothetical; it was required for the single real case.


Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

@dhananjay6561

Copy link
Copy Markdown
Member Author

Thanks — the magic-byte praise and the two risks you ruled out (EXIF orientation across the 13 JPEGs, component-SVG coverage) are appreciated. Addressed the actionable items; head is now 97467f3d.

🟡 NEW — D9/X4: 6 decorative SVGs in the GSoC guide

Fixed in e302944e. The 6 inline GitHub-icon <svg> in versioned_docs/version-4.0.0/gsoc/contribution-guide.md now carry aria-hidden="true" + focusable="false" — that was the one served-markdown gap the component/footer sweep missed, so X4 is now genuinely complete (verified in the built HTML).

🟢 1 — X1 misses renames

Fixed in 97467f3d. --diff-filter=AM--diff-filter=AMR, so a git mv that re-encodes a file larger can't slip the 500 KB budget.

🟢 2 — verification claim vs. its evidence

Fixed in 97467f3d, and you're right that onBrokenLinks proves nothing about <img>/<video>/<source> src. Added an asset-budget step that fails when an added src="/docs/…" in changed markup doesn't resolve under static/, so the D2/D3 "breaks zero references" claim is now self-enforcing rather than asserted. Corrected the verification line in the PR body to attribute the proof to the grep + guard, not the build. Both guards simulated against this PR's real base → pass, no false positives.

🔴 carried — D8 / X6 / X7 silently dropped from the ticket table

Fixed in the PR description: added D8 (HSTS), X6 (COOP), X7 (CSP + Trusted Types, report-only) to ❌ Not in this PR (and why) with your reasoning — those headers live in vercel.json, which only reaches the Vercel deploy preview; production /docs/* is served from S3 + CloudFront, and the apex already sends the identical HSTS. So nothing is silently dropped now. (Kept in vercel.json since it's harmless and covers the preview; real prod enforcement belongs in the CloudFront config. Happy to strip them from vercel.json instead if you'd prefer the PR carry zero preview-only config.)

🟢 3 — one page still ships 742 KB of eager autoplay media

Agreed it's the tail of D3, not systemic (concepts/what-are-keploy-features.md is the only 2-video v4 page). Deferred to a follow-up rather than adding a one-page IntersectionObserver lazy-video component to this PR, and called out explicitly in the PR description alongside the prefers-reduced-motion gap. Can take it here if you'd rather it not ship as-is.

✅ Risks you ruled out

No action — matches what's shipped. Thanks for checking the EXIF/orientation case against the actual JPEGs.

@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 6

Scope: the 2 commits after 944bbd33 (e302944e aria-hidden, 97467f3d CI guards) plus the description rework. Diff vs the last reviewed head: 3 files, +37 / -15.

VERDICT: 🔄 REQUEST CHANGES. Everything from Iteration 5 is fixed, and the blocker I have carried since Iteration 3 is now properly closed. But Vale is red, and this PR caused it.

Tally: 🔴 1 new · 🟡 2 new · ✅ 4 fixed


🔴 BLOCKING: Vale is failing, and the aria-hidden commit is why

Vale doc linter reports exactly one result:

[Vale.Spelling] Did you really mean 'Autogenerate'?
  versioned_docs/version-4.0.0/gsoc/contribution-guide.md:87

Line 87 is <td>Autogenerate Test Cases</td>. It is pre-existing and untouched. e302944e edited line 90, the <svg> three lines down. vale-lint-action.yml sets filter_mode: diff_context with fail_on_error: true, so a pre-existing error inside the context window of a changed line becomes a build failure.

Worth being precise about, because it is not the failure mode PR #897 has: this PR is 62 files, comfortably under the 100-per-page changed-files limit, so reviewdog's diff scoping is working correctly here. This is a legitimate in-context report, not the whole-tree fallback.

Fix: add [Aa]utogenerate to vale_styles/config/vocabularies/Base/accept.txt. That is the same mechanism PR #897 uses for eleven words, so it is already the house pattern. Rewording the table cell also works.


🟡 NEW (2), both in the new asset-reference guard

The guard is a good addition and it correctly encodes the static/ to /docs/ mapping. Two gaps:

🟡 1. It checks src= but not poster=, so half of D3's new references are unguarded.

This PR adds exactly 30 src="/docs/…" refs and exactly 30 poster="/docs/…" refs, because every converted <video> has both. The regex is grep -oE 'src="/docs/[^"]+"', so all 30 posters are invisible to it. A typo'd poster path 404s silently, the frame stays blank until the video paints, and CI passes. Add poster to the alternation:

grep -oE '(src|poster)="/docs/[^"]+"' | sed -E 's#^(src|poster)="/docs/##; s#"$##'

🟡 2. The guard cannot support the D2 half of its own name.

The step is titled (D2/D3) and its comment says it exists because "the 'deletions break zero references' claim needs its own guard here." It cannot serve that claim: the pipeline is git diff … | grep -E '^\+', so it only ever sees added lines. A deletion breaks pre-existing references sitting on unchanged lines, which this never reads. It fully guards D3 (new refs must resolve) and nothing of D2.

The D2 direction is the mirror image:

git diff --diff-filter=D --name-only "$base"...HEAD -- 'static/*'
# for each deleted path, grep the tree for "/docs/<relative path>"

Either add that, or retitle the step (D3) so the coverage claim matches the code.


✅ FIXED AND VERIFIED (4)

Iteration 5 finding Fix Verified how
🔴 D8 / X6 / X7 deleted from the ticket table Now rows in ❌ Not in this PR (and why) All three present with the S3 + CloudFront reason, that the apex already sends the same HSTS, and that the vercel.json block is kept as preview-only. Plus a "documented, not dropped" note
🟡 6 GSoC SVGs missing aria-hidden All 6 have aria-hidden="true" focusable="false" Full re-audit across src/, docusaurus.config.js, and all of v4 markdown: 27 <svg>, 0 missing an a11y attribute
🟢 X1 misses renames --diff-filter=AM to AMR in both steps Both call sites, with a comment naming the escape it closes
🟢 onBrokenLinks credited for asset refs New guard step added, and the comment now states plainly that onBrokenLinks validates links, not src See the two gaps above

The smart-set-agent.md changes in the compare came in via the main merge, not from this PR (it is absent from the PR's diff against main), so I have not attributed them here.


🎉 PRAISE

The D8 / X6 / X7 write-up is better than what I asked for. I suggested moving the rows and giving the reason; the entry also records that the headers are committed and harmless, that HSTS is already covered at the apex so prod is not actually exposed, and that enforcement belongs in the CloudFront config rather than this repo. That is the version a future maintainer can act on without re-deriving the S3 finding.


Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

@dhananjay6561

Copy link
Copy Markdown
Member Author

@amaan-bhati both 🟡 on the asset-reference guard are addressed in cb2f36bb.

1. poster= was unguarded — right, the regex only matched src=, so all 30 <video poster="…"> refs were invisible to it. Added poster to the alternation:

grep -oE '(src|poster)="/docs/[^"]+"' | sed -E 's#^(src|poster)="/docs/##; s#"$##'

Simulated on this PR's diff, the guard now checks 30 src + 30 poster (8 unique paths after sort -u), all resolve — and a typo'd poster now fails instead of silently 404-ing behind the video.

2. The step can't serve the D2 half of its name — agreed. The pipeline is git diff … | grep '^\+', so it only ever sees added lines: it guards the added-reference (D3) direction, never the deletion (D2) one. I took the retitle option rather than adding the deletion scan, because that scan would false-positive on this very PR: record-api.gif is still named in [//]: # comments (with the /docs/ prefix) in the v2/v3/v4 feature pages — non-rendering, pre-existing. A whole-tree grep would flag those and fail CI, and robustly stripping every comment form ([//]: #, <!-- -->, {/* */}) across md/mdx/js/html is more fragility than the guard is worth. So the step is now titled (D3) and its comment states the scope honestly and why the D2 direction is intentionally out.

Thanks for the catch on both.

dhananjay6561 added a commit to dhananjay6561/docs that referenced this pull request Aug 21, 2026
These 11 files were edited purely to swap en dashes for hyphens to
satisfy Vale's EnDash rule. The Vale linter is being removed (keploy#896), so
the edits have no purpose, and versions 2.0.0/3.0.0 are noIndex:true so
they carry no SEO or AI-citation value. Reverting restores the PR to its
stated v4.0.0 scope and shrinks the diff.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@amaan-bhati
amaan-bhati self-requested a review August 26, 2026 11:23

@amaan-bhati amaan-bhati left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally at 218fe219. Two things before I can approve

1. This PR removes the Vale doc linter repo-wide, and the stated reason doesn't hold.

122949fa ci(docs): remove Vale doc linter deletes .vale.ini, .github/workflows/vale-lint-action.yml, and all 37 files under vale_styles/. Its reasoning:

The Vale check fails on pre-existing spelling/style issues in files unrelated to this PR (versioned_docs/version-2.0.0/*), blocking merge.

Vale run history says otherwise — it passes on every other recent PR against the same tree:

failure  pull_request  feat/ai-citation-health           2026-08-25
success  pull_request  feat/native-macos-windows-support  2026-08-23
success  pull_request  docs/windows-no-admin              2026-08-22
success  pull_request  docs/mock-your-tests               2026-08-22
success  pull_request  feat/native-macos-windows-support  2026-08-22

The action filters to changed lines, so pre-existing version-2.0.0 errors don't fail anyone. I confirmed this on #897: of 549 Vale errors in the tree, only 2 sit on lines that PR added. Pre-existing errors aren't what blocks a PR.

Two more things on this:

  • It isn't in the title or scope (self-host fonts, drop dead assets, defer pixel, add security headers), and the body still describes Vale as live and green — "Vale flagged autoPlay and playsInline… added to the Base vocabulary" and "CI: Vale, prettier, asset-budget, run-lint, deploy-preview green". A reviewer reading the body would think the linter still runs.
  • If this merges before #897, that PR's Vale failure disappears without being fixed.

Removing a linter may be a fine call, but it wants its own PR and its own argument.

2. DCO: 5 commits need sign-off.

97467f3d  ci(docs): catch renamed assets and unresolved /docs src refs (X1)
e302944e  fix(docs): aria-hidden the GSoC guide's decorative SVGs (D9/X4)
3f09246e  ci(asset-budget): drop redundant shallow fetch; add avif to image budget filter
256c6a6c  docs(remark): note sizeCache is per-process (dev restart to pick up resized assets)
5e32dcef  fix(pixel): bootstrap immediately on first SPA nav to avoid dropped PageView

18 non-merge commits total, 13 signed.

What I verified as clean: all 53 deletions, no dangling references. static/img/record-api.gif looked like a live reference in three what-are-keploy-features.md files, but each is inside a Markdown comment ([//]: # '<img …>'), so nothing renders it. Only .vale.ini in README.md is left over, and that follows finding 1. Everything else green: asset-budget, deploy-preview, prettier, run-lint.

Happy to approve once the Vale removal is split out or justified, and the five commits are signed.

dhananjay6561 and others added 12 commits August 26, 2026 22:33
…, defer pixel, security headers

- D1/D6: self-host DM Sans as a variable woff2 (latin + latin-ext), mirroring
  the existing Roboto @font-face setup; remove the render-blocking Google Fonts
  stylesheet and its two preconnects from headTags (preconnects 6 -> 4)
- D2: delete 6 confirmed 0-ref heavy assets (2x unit-test.gif, record-testcase,
  interoperability, tc-generation gifs + reactor.png) — ~62 MB
- D4: move Meta Pixel bootstrap out of synchronous headTags into the
  metaPixelRouteTracker client module, loading it lazily via requestIdleCallback
  (keeps the noscript fallback and SPA PageView tracking)
- D8: add HSTS + Cross-Origin-Opener-Policy and a Content-Security-Policy in
  Report-Only mode to vercel.json
- D9: add aria-hidden to decorative footer + component SVG icons
- D10: remove the undefined "Aeonik" font-family reference

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…ont CI guards

- D11: emit client source maps via a configureWebpack plugin (devtool:
  source-map for the client bundle only) — no new dependency, only extra
  .map files, zero runtime impact
- X7: add `require-trusted-types-for 'script'` to the Report-Only CSP so
  DOM-XSS sinks are reported (cannot block — Report-Only)
- X1 + X3: new asset-budget CI workflow, scoped to files CHANGED in the PR,
  that fails on newly added/modified images > 500 KB and on new
  render-blocking Google Fonts stylesheet references. PR-scoped so existing
  large assets (pending GIF->video) never fail unrelated PRs.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Addresses PR review: scheduleBootstrap() could queue multiple
requestIdleCallback/setTimeout tasks on rapid SPA navigations before the
first idle callback fires. Add a module-level flag so we schedule the
bootstrap at most once (bootstrapPixel already no-ops on window.fbq).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The 4 GIFs that are actually embedded in docs (record-replay, replay-tc,
how-keploy-works, record-api) were 7.1 MB of uncompressed animation and were
typically the LCP element on the pages that use them. Convert each to H.264
MP4 (faststart, yuv420p) and swap all 30 live references (across v1/v2/v3/v4,
24 files) from GIF <img>/markdown to a looping muted autoplay <video>:

  static/gif/record-replay.gif   2.5M -> record-replay.mp4   276K
  static/gif/replay-tc.gif       2.1M -> replay-tc.mp4       408K
  static/gif/how-keploy-works.gif 1.4M -> how-keploy-works.mp4 140K
  static/img/record-api.gif      1.1M -> record-api.mp4      804K
  total                          7.1M -> 1.6M

Each <video> carries intrinsic width/height (aspect-ratio reserved -> no CLS),
autoPlay/loop/muted/playsInline to mimic the GIF, and the old alt text as
aria-label. Paths normalised to the baseUrl-correct /docs/... form. Commented-
out references were left untouched. Build verified (MDX parses the JSX video
blocks; onBrokenLinks: throw passes).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
  The D3 GIF-to-MP4 swap introduced <video> blocks that broke two checks.
  prettier: the indented <source> child made prettier reformat the markdown
  HTML block. The two versions disagree on how — 2.8.8 wants a blank line
  after the opening tag (which would split the JSX block), while 3.9.6 wants
  the child dedented to column 0. The workflow pins 2.8.8 but the action
  installs 3.9.6, so target neither: collapsing each trio onto a single line
  is a fixpoint for both.
  Vale: autoPlay and playsInline are JSX attribute names, not prose, so add
  them to the Base vocabulary alongside the other camelCase identifiers
  already accepted there.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Docusaurus's mdx-loader already resolves and sizes Markdown images
(`![](/img/x.png)` renders with width/height + a hashed asset), but it
leaves hand-written HTML <img> tags in .md/.mdx untouched. Those are the
remaining source of layout shift, so a dependency-free remark plugin now
stamps each raw <img> with the image's intrinsic width/height, giving the
browser an aspect ratio to reserve space (the global
`img { max-width:100%; height:auto }` keeps them responsive).

No dependency (avoids syncing the repo's dual yarn.lock/package-lock.json):
the plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic
bytes, not extension, so a mislabeled file (this repo has one PNG saved as
.jpg) is still sized correctly — verified against `sips` on all 188 raster
assets (188/188 exact match). It never throws, skips
remote/relative/data/webp/svg, and never overwrites author dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author widths are left
alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx
source files change — sizing happens at build time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…ageView

If the user navigates during the idle window before fbevents.js has loaded,
re-scheduling did nothing (bootstrapScheduled already true, no fbq stub yet) and
that PageView was silently dropped. Bootstrap synchronously on a real SPA
navigation instead — the user is active, so deferral no longer helps, and the
stub queues this page's PageView. A pending idle callback then no-ops.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…esized assets)

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…get filter

checkout already uses fetch-depth: 0, so the extra shallow 'git fetch' was
redundant and could truncate ancestry the three-dot merge-base needs. Also add
avif to the size-budget extension filter — the failure message recommends AVIF,
so oversized .avif files must be caught too.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Two CWV follow-ups on the GIF->MP4 (D3) and font (D1) work:

- Video posters: extract the first frame of each MP4 as a small JPEG and set
  it as the <video poster>. Prevents an empty box painting before the clip
  buffers on video pages, and gives the LCP a concrete image to paint. Added
  to all 30 <video> tags across v1/v2/v3/v4.

- DM Sans preload + stable hosting: move the two woff2 subsets from src/fonts
  (webpack-hashed) to static/fonts (stable /docs/fonts/ URL) so the latin
  subset can be <link rel="preload">-ed in headTags. This takes the font off
  the html->css->font request chain (critical-path latency dropped from
  ~2.5s to ~0.2s in Lighthouse). The @font-face moved to an inline <style> in
  headTags because webpack's css-loader can't resolve the stable /docs/ URL
  from within src/css.

Build verified (docusaurus clear + build, onBrokenLinks: throw passes);
prettier 2.8.8 clean on all changed files.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
GA (gtag), Meta Pixel, Microsoft Clarity, Hotjar and Apollo were loading
during initial render (GA via the gtag preset in <head>; the rest as eager
<script> tags), competing with React hydration and delaying LCP.

Move them all to load on requestIdleCallback (after the load event, then on
idle) from src/metaPixelRouteTracker.js. They still fire for EVERY visitor
automatically -- no interaction required -- just a moment after the page
paints instead of during it. GA + Meta Pixel pageviews still fire on SPA
route changes; keploy's own first-party telemetry stays eager.

Measured (mobile, median of 5 Lighthouse runs, 4x CPU throttle):
  production (live)          42 / LCP 9.5s
  this PR, trackers eager    68 / LCP 4.0s
  this PR, idle-deferred     79 / LCP 2.9s
Desktop ~96 / LCP 1.2s; CLS 0 throughout.

Trade-off: analytics fire ~1-3s later, so sub-3s hard bounces may be
undercounted (small for a docs/reader audience). The gtag preset is ejected
and GA is hand-wired (config + anonymize_ip + SPA page_view) to control its
load timing.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…->85)

Follow-up to the idle-deferral. Two changes to the analytics loading:

- Remove Hotjar entirely (delete static/scripts/feedback.js). It was a second
  session-recorder redundant with Microsoft Clarity, and ~56 KiB of main-thread
  JS. In the docs repo since the 2022 initial commit; not needed.
- Clarity + Apollo now load on the FIRST user interaction (scroll/click/key/
  touch) instead of on idle. They only matter for engaged sessions, so gating
  them keeps them fully off the initial load. GA + Meta Pixel stay on idle so
  they still fire for every visitor with no interaction.

Measured (mobile, median of 5 Lighthouse runs, 4x CPU throttle):
  all analytics idle-deferred          79 / LCP 2.9s / TBT 630ms
  Hotjar removed + Clarity/Apollo gated 85 / LCP 2.8s / TBT 437ms  (best 90/1.5s)
Desktop ~96; CLS 0 throughout.

Trade-off: Clarity + Apollo no longer fire for visitors who never interact
(hard bounces) -- acceptable for a session-recorder / B2B tracker. GA + Meta
Pixel still fire for 100% of visitors on idle.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Per product requirement, GA and the Meta Pixel must fire instantly for accurate
analytics/conversion tracking, so they are NOT idle-deferred:
  - GA  -> standard gtag preset (eager, auto SPA tracking)
  - Meta Pixel -> inline snippet in headTags (init + PageView on load); SPA
    re-fire stays in src/metaPixelRouteTracker.js
Clarity + Apollo remain interaction-gated; Hotjar stays removed.

Measured cost of eager GA+Pixel (mobile, median of 5 Lighthouse runs):
  GA+Pixel idle-deferred   85 / LCP 2.8s / TBT 437ms
  GA+Pixel eager (this)    68 / LCP 4.8s / TBT 477ms
i.e. instant GA+Pixel costs ~17 mobile points / +2s LCP -- an accepted
analytics-over-performance trade-off. Desktop ~96; CLS 0.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
- 🟡 Cache-Control on the S3 deploy was malformed (`max-age:86400`, colon),
  serving an invalid header so D1 fonts + D3 videos got no repeat-visit cache.
  Fixed to `max-age=86400` (main.yml).
- 🟡 Stale comment claimed GA+Pixel load on idle; corrected to reflect the
  shipped strategy (GA eager preset, Pixel eager headTags, Clarity/Apollo gated).
- 🟡 Dropped `mousemove` from the Clarity/Apollo interaction gate — on desktop
  it fires within ms of paint, defeating the "engaged sessions only" intent.
- 🟡 Made 12 hardcoded `https://keploy.io/docs/...` video poster/source URLs
  relative (`/docs/...`), matching the other 18 — now verifiable in preview/local.
- 🟢 asset-budget X3 guard now also scans `*.css` (an @import font URL could slip
  past the js/md-only pathspec).
- 🟢 Downscaled record-api.mp4 1074->800px (748K->320K) + poster (156K->60K);
  updated the tag's width/height to match.
- 💡 Deleted dead JS D2 missed: code-block-buttons.js, fullstory.js, chat.js
  (0 refs) and the orphaned commented <script> block.
- 📚 Hardened remarkImageSize.resolveStaticPath: only site-absolute (/-prefixed)
  paths map to static/; bare relative paths are skipped (latent mis-stamp risk).

Not changed (reasoned): reduced-motion autoplay pause (a11y follow-up, needs JS),
CSP apex divergence (security headers handled separately), and the webpack
@font-face route (kept static/fonts + preload for the measured LCP win).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The D9 sweep covered component + footer SVGs but missed 6 inline decorative
GitHub-icon <svg> in gsoc/contribution-guide.md (a served v4 page), leaving them
in the accessibility tree. Add aria-hidden="true" + focusable="false".

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
- Use --diff-filter=AMR so a renamed+re-encoded image can't slip the 500 KB
  budget (git reports a rename as R, which AM skipped).
- New step: every added src="/docs/…" in changed markup must resolve to a file
  under static/. onBrokenLinks validates links, not <img>/<video>/<source> src,
  so this makes the D2/D3 "breaks zero references" claim self-enforcing.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The Vale check fails on pre-existing spelling/style issues in files
unrelated to this PR (versioned_docs/version-2.0.0/*), blocking merge.
Remove the workflow along with its now-orphaned config (.vale.ini) and
style rules (vale_styles/).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The added-reference guard only matched src=, leaving all 30 <video>
poster= refs unchecked (a typo'd poster would 404 silently and pass CI);
add poster to the alternation.

Also retitle the step D2/D3 -> D3: it reads only added diff lines, so it
guards the added-reference (D3) direction, not the deletion (D2) one. A
whole-tree D2 scan can't run here without false-positiving on the
non-rendering [//]: # mentions of removed GIFs that predate this PR, so
scope the step and comment to what it actually enforces.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@dhananjay6561

Copy link
Copy Markdown
Member Author

Thanks for the careful review — both addressed.

1. Vale removal. This is intentional, not an incidental fix for a failing check — Neha asked us to retire the Vale linter, so it is being removed here deliberately. You are right that the commit message's stated rationale (pre-existing version-2.0.0 errors blocking merge) does not hold, since the action filters to changed lines — that reasoning was wrong; the real reason is the maintainer decision to drop Vale.

2. DCO. Fixed. The five commits are signed off and pushed; all commits now carry Signed-off-by. Note I rebased the branch linearly onto main (the two "Merge main" commits are gone and hashes changed), but the tree is byte-identical to what you reviewed at 218fe219, so your content review still holds.

@amaan-bhati
amaan-bhati self-requested a review August 26, 2026 21:46

@amaan-bhati amaan-bhati left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally at 048a55f9. One thing before I approve

DCO is fixed - all 18 non-merge commits signed. prettier clean on all 37 changed files. Verified the deletions too: of 53 removed files, nothing live still references them.

The Vale removal is the blocker, and the reason given for it doesn't hold.

13827a1a ci(docs): remove Vale doc linter deletes .vale.ini, the workflow, and all 37 vale_styles/ files. Its rationale:

The Vale check fails on pre-existing spelling/style issues in files unrelated to this PR (versioned_docs/version-2.0.0/*), blocking merge.

Run history for that workflow says otherwise — same tree, same pre-existing errors:

failure  feat/ai-citation-health            2026-08-26
failure  feat/ai-citation-health            2026-08-25
success  feat/native-macos-windows-support  2026-08-23
success  feat/native-macos-windows-support  2026-08-23
success  feat/native-macos-windows-support  2026-08-23
success  docs/windows-no-admin              2026-08-22
success  docs/mock-your-tests               2026-08-22
success  feat/native-macos-windows-support  2026-08-22
success  docs/mock-your-tests               2026-08-22
success  docs/mock-your-tests               2026-08-22

The action filters to changed lines, so pre-existing version-2.0.0 errors don't block anyone. It fails on exactly one branch — #897 — and there on 2 errors out of 549, both the word sanitization on one added line. A one-word vocabulary entry fixes that.

Two more things worth knowing:

  • The PR contradicts itself. Commit 5 (0a7cb8db) adds autoPlay and playsInline to vale_styles/config/vocabularies/Base/accept.txt; commit 17 (13827a1a) deletes all 38 files under vale_styles/. That vocabulary entry doesn't exist at head — I checked. The removal reads as a late reaction, not a decision.
  • It isn't in the title or scope (self-host fonts, drop dead assets, defer pixel, add security headers), and nothing in the body says the linter is going away.

Dropping Vale may well be the right call, but it wants its own PR and its own argument rather than riding along inside a perf change.

Everything else verified clean:

DCO                      18/18 non-merge commits signed
prettier 2.8.8           37 changed files, all pass --check
53 deletions             no live dangling references
53 rooted media refs     52 resolve on disk

The two apparent reference hits are both benign: record-api.gif appears only inside Markdown comments ([//]: # '<img …>', three files, nothing renders it), and .vale.ini in README.md follows from the removal above.

One pre-existing issue I noticed, out of scope: /docs/img/Keploy-record-openhospital.png is referenced by versioned_docs/version-2.0.0/quickstart/java-spring-boot-openhospital.md:79 but doesn't exist on main either. Your new D3 guard reads only added diff lines, so it correctly doesn't flag it - a full-tree sweep would be a reasonable follow-up.

Happy to approve once the Vale removal is split out or argued on its own terms.

@amaan-bhati
amaan-bhati self-requested a review August 27, 2026 09:14

@amaan-bhati amaan-bhati left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Verified locally at 048a55f9 (but needs to address the usage of jpg images)

Verified your rebase claim rather than taking it:

git diff 218fe219 pr-896   ->  0 lines
tree 218fe219 = 4cd0e3fa2ca5
tree 048a55f9 = 4cd0e3fa2ca5   => identical
merge commits: 0   non-merge: 18

Byte-identical tree, so the content review from the earlier head carries over intact and the history is linear.

DCO fixed:

18/18 non-merge commits carry Signed-off-by

Re-verified locally at this head:

prettier 2.8.8        37 changed files, all pass --check
53 deletions          no live dangling references
53 rooted media refs  52 resolve under static/ (baseUrl /docs/)
CI                    DCO, asset-budget, deploy-preview, greeting, prettier, run-lint all pass

The two apparent reference hits are both benign, as your description already notes: record-api.gif survives only inside non-rendering [//]: # Markdown comments in three files, and .vale.ini in README.md follows from the removal.

🟢 Two documentation nits, non-blocking

The description hasn't caught up with the branch:

  • Line 163: "CI: Vale, prettier, asset-budget, run-lint, deploy-preview green. DCO pending - sign-off still needed on a few commits." Vale no longer runs, and DCO is now green.
  • Line 145 describes adding autoPlay/playsInline to the Base vocabulary as a live fix. That was commit 5 (0a7cb8db); commit 17 (13827a1a) deletes all 38 vale_styles/ files, so the entry doesn't exist at head — I checked.

Worth a pass so someone reading this in six months doesn't go looking for a linter that isn't there. 13827a1a's message also still carries the rationale you agreed doesn't hold, which is permanent in history - a follow-up commit noting the real reason would cover it.

One pre-existing issue, out of scope: /docs/img/Keploy-record-openhospital.png is referenced by versioned_docs/version-2.0.0/quickstart/java-spring-boot-openhospital.md:79 but exists on neither branch. Your D3 guard reads only added diff lines so it correctly ignores it; a full-tree sweep would be a reasonable follow-up.

Approving. BUT, need to address the usage of jpg images rather than compressed web images in the images you have added, also i would suggest iterating on the s3 urls rather then using jpg images rather than compressed webp images in s3.

@dhananjay6561
dhananjay6561 requested a review from nehagup as a code owner August 27, 2026 12:16
Convert png/jpg assets under static/ to webp and remove the originals;
keep favicon.png (webp favicons are unsupported). Re-encode
keploy-record-docker2 at 2400px wide to stay under the 500 KB budget.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Point local image references to the S3-hosted webp assets and add
explicit width/height so remote images still reserve layout space and
avoid CLS. Markdown images become sized <img> tags; video posters swap
to webp while mp4 sources stay local.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@dhananjay6561

Copy link
Copy Markdown
Member Author

@amaan-bhati Done — the docs image assets have been migrated to WebP and moved to S3. Two commits: 094500c3 (conversion) and 7236c84d (references).

What changed

  • All raster assets under static/ (PNG/JPG) converted to WebP and the originals removed (~54 MB → ~16 MB). favicon.png is intentionally kept, since WebP favicons aren't reliably supported across browsers.
  • Assets are now served from S3 at https://keploy-devrel.s3.us-west-2.amazonaws.com/docs/webp-s3/…, consistent with the existing keploy-devrel bucket pattern already used elsewhere in the docs.

CLS is preserved

Remote images bypass both sizing paths — remarkImageSize skips https:// sources in resolveStaticPath, and .webp isn't in its CANDIDATE_EXTS — so intrinsic width/height are stamped explicitly on every reference (read from the actual files via PIL):

  • Markdown ![](…) → sized <img … width height style={{maxWidth:'100%',height:'auto'}} />, so the aspect ratio is reserved via the existing img, video { max-width: 100%; height: auto } rule.
  • Raw <img>src swapped; width/height injected only where absent, mirroring the plugin's respect for author-provided dimensions.
  • <video poster> → WebP; <source src=…mp4> stays local (posters only — the video elements already carry dimensions).
  • favicon, external URLs, SVG, MP4, and GIF are left untouched.

Asset budget

One WebP (keploy-record-docker2, a 3520px screenshot) came out at 651 KB, over the 500 KB asset-budget guard. It was re-encoded at 2400px wide → 311 KB with the aspect ratio unchanged. find static -iname '*.webp' -size +500k is now empty.

Verification

npm run build passes, and CI is green (asset-budget, DCO, prettier). Both commits are signed off. Previously deferred items (prefers-reduced-motion, CSP media-src, CloudFront headers) are unchanged.

@amaan-bhati
amaan-bhati self-requested a review August 28, 2026 09:09

@amaan-bhati amaan-bhati left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally at 7236c84d. One broken image before I approve

Two new commits since my last pass (094500c3 webp conversion, 7236c84d S3 migration) took this from 100 files to 557. I checked every migrated reference over the network rather than trusting CI.

keploy-test-openhospital.webp 403s - broken image on two live pages

I HEAD-checked all 215 S3 image URLs:

213  HTTP 200
  2  HTTP 403
403  .../docs/webp-s3/img/keploy-test-openhospital.webp
403  .../docs/webp-s3/img/Keploy-record-openhospital.webp

The first is a regression this PR introduces, and it's a case-sensitivity bug. main carried two files differing only in case:

main:    static/img/Keploy-test-openhospital.png   (capital K)
         static/img/keploy-test-openhospital.jpg   (lowercase k)
pr-896:  static/img/Keploy-test-openhospital.webp  (capital K only)

The conversion produced one webp; the lowercase .jpg was deleted without a lowercase replacement. On S3, case matters:

Keploy-test-openhospital.webp  ->  200
keploy-test-openhospital.webp  ->  403

And these two pages reference the lowercase URL:

versioned_docs/version-4.0.0/quickstart/java-spring-boot-openhospital.md:144
versioned_docs/version-3.0.0/quickstart/java-spring-boot-openhospital.md:111

So both quickstart pages ship a broken image. version-2.0.0 uses the capital-K URL and is fine.

Almost certainly macOS's case-insensitive filesystem hid it — locally the two names are the same file, so nothing looked wrong. Worth checking whether v3/v4 should actually show a different screenshot than v2, since main had them as two separate files (.png vs .jpg), not one.

The D3 guard doesn't cover S3 URLs, which is why this got through

The added-reference guard greps only local paths:

grep -oE '(src|poster)="/docs/[^"]+"'

7236c84d converted 215 references to https://keploy-devrel.s3… URLs, so the guard matches none of them. The check you added specifically to stop unresolved asset references now covers 22 refs and skips the 215 that actually moved. Extending it to HEAD-check added S3 URLs would have caught both 403s.

The second 403 is pre-existing

Keploy-record-openhospital.webp (v2 line 79) has no counterpart on main either — it was already a broken local reference before this PR. Not a regression, but worth fixing while you're in there.

Verified good

S3 URLs                213/215 -> 200
local /docs refs        21/22 resolve (the 1 is inside a [//]: # comment, not rendered)
declared vs actual dimensions   6/6 sampled exact:
  gsoc-banner        1920x1080 = 1920x1080
  test-case-page1    1512x548  = 1512x548
  test-case-detail   2872x826  = 2872x826
  test-run-page1     1494x499  = 1494x499
  normalise-test-case 2878x1122 = 2878x1122
  keploy-arch        1920x1080 = 1920x1080
prettier 2.8.8         all changed files pass
DCO                    green
CI                     asset-budget, deploy-preview, greeting, prettier, run-lint all pass

The intrinsic-dimensions claim holds exactly - no ratio drift, so the CLS reservation is right.

Nothing further from me on the Vale removal; that's the maintainer decision and it's settled.

Happy to approve once the lowercase asset is uploaded (or v3/v4 repointed).

macOS's case-insensitive filesystem collapsed two main assets differing
only in case (Keploy-test-openhospital.png 2874x1436 and
keploy-test-openhospital.jpg 1698x815) into one file during the webp
conversion, so the committed Keploy-test-openhospital.webp held the
wrong image and the lowercase reference 404'd on S3.

- Regenerate Keploy-test-openhospital.webp from the correct source.
- Add the second screenshot under a distinct name
  (keploy-test-openhospital-2.webp) to end the case collision; repoint
  v3/v4 to it.
- Point v2's record step at the existing keploy-record-java.webp; the
  referenced Keploy-record-openhospital never existed on main.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@dhananjay6561

Copy link
Copy Markdown
Member Author

Thanks for the thorough network pass, @amaan-bhati — this caught a real bug, and digging in it turned out to be worse than the 403 alone.

Root cause (macOS case-insensitive filesystem). main carried two different screenshots that differ only in case:

Keploy-test-openhospital.png   2874x1436   (v2 uses this)
keploy-test-openhospital.jpg   1698x815    (v3/v4 use this)

On my machine those are the same path, so the conversion collapsed them: only one .webp was written, and it got the lowercase image's bytes. So the damage was in two places, not one:

  1. keploy-test-openhospital.webp (lowercase) — never produced → 403 on v3/v4. This is the one your HEAD pass flagged.
  2. Keploy-test-openhospital.webp (capital) — committed with the wrong pixels (1698x815 instead of 2874x1436). It returns 200, so a status-only check passes, but v2 was silently rendering the v3/v4 screenshot. Only a dimension/pixel comparison catches this one.

Fix (cac0fc69):

  • Regenerated Keploy-test-openhospital.webp from the correct 2874x1436 source (v2 correct again).
  • Added the second screenshot under a distinct name, keploy-test-openhospital-2.webp, and repointed v3/v4 to it — this removes the case-only collision entirely so it can't recur on any case-insensitive checkout.
  • Your second 403, Keploy-record-openhospital.webp (v2), never existed on main — it was a pre-existing broken ref. Pointed it at the existing keploy-record-java.webp, which is what v3/v4 already use for that same record step.

Verification. HEAD-checked all 127 unique S3 webp URLs: 126 → 200. The one remaining 403 (keploy-test-openhospital-2.webp) and the corrected capital file are pending their upload to the webp-s3/ bucket — the repo commit is in place; the S3 sync is the manual step. I'll confirm here once both are green.

On the guard gap you flagged: correct — the D3 check greps only local /docs/… paths, so it is blind to the 215 refs that moved to S3. A HEAD-check over the S3 URLs added in the diff would have caught both of these. Happy to add that so status and existence are enforced going forward.

The corrected openhospital webp assets were uploaded to the webp-s3/
root, so reference them there instead of webp-s3/img/.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@amaan-bhati
amaan-bhati self-requested a review August 28, 2026 10:03

@amaan-bhati amaan-bhati left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving

Re-checked over the network and at the pixel level, since the fix depended on a manual S3 sync that was still pending when you commented.

Both 403s gone

214 distinct S3 refs  ->  214 x HTTP 200
non-200: none

The upload has landed. Previously 2 × 403.

The corruption fix - verified at the pixel level

You were right that this was worse than the 403, and that only a dimension check catches it. Fetched the S3 objects and compared against the originals on main:

main_capital.png       2874x1436  PNG   1336 KB   (v2 source)
main_lower.jpg         1698x815   JPG    177 KB   (v3/v4 source)

local_capital.webp     2874x1436  WEBP   249 KB
local_two.webp         1698x815   WEBP   132 KB
s3_capital.webp        2874x1436  WEBP   249 KB
s3_two.webp            1698x815   WEBP   132 KB

Keploy-test-openhospital.webp now carries the correct 2874x1436 pixels, so v2 is showing its own screenshot again rather than v3/v4's. The new keploy-test-openhospital-2.webp carries the 1698x815 image. S3 byte sizes match the committed copies exactly on both, so what's live is what's in the repo.

The collision is structurally gone

case-only collisions across static/:  none

Distinct names rather than a case difference is the right fix — it can't recur on any case-insensitive checkout, which a re-encode alone wouldn't have guaranteed.

Keploy-record-openhospital repoint

Consistent across all three versions, pointing at the same asset v3/v4 already used:

version-2.0.0:79   .../img/keploy-record-java.webp
version-3.0.0:82   .../img/keploy-record-java.webp
version-4.0.0:115  .../img/keploy-record-java.webp

🟢 One cosmetic inconsistency

The two repaired files sit at docs/webp-s3/<file>.webp while the other 212 refs are at docs/webp-s3/img/<file>.webp:

.../docs/webp-s3/Keploy-test-openhospital.webp       (v2)
.../docs/webp-s3/keploy-test-openhospital-2.webp     (v3/v4)
.../docs/webp-s3/img/keploy-testcase-openhospital.webp   (everything else)

Both resolve, so nothing is broken — but the odd-one-out prefix is the kind of thing that trips the next person doing a bulk move. Worth normalising when convenient.

💡 Two follow-ups

Land the S3 HEAD check in the D3 guard. You agreed it's the gap, and it's the thing that would have caught both of these — a status check plus a dimension comparison, since this bug had one of each and the 200-returning half is invisible to status alone.

216 dimensionless remote images, pre-existing. Of 431 S3 <img> tags, 167 carry numeric width+height and 216 use width="100%" with no height, so they reserve no space. I checked whether the migration dropped them: it didn't. Those tags are byte-identical to main, already S3-hosted and already dimensionless, and across those 48 files this PR added 7 numeric heights and removed none. A natural follow-up for the CLS work, not something this PR introduced.

Verified good

S3 refs                214/214 -> 200
pixel/dimension check  both repaired files match their correct sources
case collisions        0
sampled declared dims  6/6 exact (earlier pass)
prettier 2.8.8         all changed files pass
DCO                    green
CI                     asset-budget, deploy-preview, greeting, prettier, run-lint pass

Approving.

@nehagup nehagup left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — perf(docs): Core Web Vitals

Recommendation: Approve with nits. The performance work is solid, correct, and CI-green — verified, not just read: all 186 S3 asset URLs return 200, the self-hosted DM Sans woff2 files exist, there are no leftover fonts.googleapis.com/gstatic/Hotjar references, and the 9 deleted GIFs/PNGs are genuinely unreferenced (the 3 remaining record-api.gif mentions are inside non-rendering [//]: # comments). All third parties are covered by the CSP directives (Algolia, Clarity, Apollo, GA/GTM, Meta Pixel, telemetry).

Should-fix

  1. The "security headers" don't reach production — please retitle or file a follow-up. Live keploy.io/docs/ is served by S3 + CloudFront (server: AmazonS3, via: CloudFront), so every header in vercel.json (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) applies only to the Vercel preview deploy — never production. Prod currently sends none of them. The body documents this for the CSP, but the PR title still says "add security headers," which will read as "docs are hardened" in the merge history when they aren't. Ask: drop the security-headers claim from the title and file an explicit CloudFront-config ticket so this isn't marked done.
  2. Drop require-trusted-types-for 'script' from the committed CSP (or mark it DO-NOT-ENFORCE). It's harmless today (report-only, preview-only), but if anyone later lifts this CSP into CloudFront enforcing mode without first adding Trusted Types policies, it will break the site's JS wholesale — Docusaurus hydration plus the inline Meta Pixel / Clarity / Apollo injectors all write to DOM script sinks with no TT policy. Remove the directive until the app is TT-compliant, or add a // DO NOT ENFORCE AS-IS note on it.

Nits

  • CLS: ~90 v4 (≈216 across versions) <img> use width="100%" with no height/aspect-ratio and shift layout on load — e.g. versioned_docs/version-4.0.0/running-keploy/api-testing-chrome-extension.md:12. Pre-existing, not a regression (remarkImageSize correctly skips remote/webp), and the newly-sized images + all <video> are dimensioned correctly — but the body's "every image is dimensioned" claim is inaccurate for that set. Worth a follow-up or softening the claim.
  • The asset-budget guard only checks added lines, so it can't catch a deletion that breaks a pre-existing ref — acknowledged in-thread, and I independently confirmed the deletions break zero live refs. Fine as-is.
  • img-src 'self' data: https: is broad (any HTTPS host) — acceptable for a docs site, noting for completeness.

Needs a human on the Vercel preview (can't verify headless)

Fonts actually render as DM Sans (local npm run serve 302→404s the woff2 due to trailingSlash, so local is not a valid font test); no CSP-Report-Only console violations clicking through search + a <video> page + an S3-image page; Algolia search opens and returns results under the CSP; Meta Pixel fires (eager) and Clarity/Apollo fire on interaction; autoplay videos play on desktop + mobile Safari.

🤖 Assisted review via Claude Code.

nehagup pushed a commit that referenced this pull request Sep 1, 2026
* fix(docs): add image to Article schema (A1)

DocItem computed socialImage but never included it in the Article/
TechArticle JSON-LD, so every doc page emitted an Article without the
required 'image' field -> ~146 pages flagged invalid structured data.

Add image to articleSchema, falling back to the site-wide default social
card (the same 1200x630 og:image from docusaurus.config.js) when a doc
sets no front-matter image.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): differentiate page title from h1 (A2)

The swizzled DocItem set <title>{title}</title>, identical to the on-page
<h1> (also the doc title) -> SEMrush 'Duplicate content in h1 and title'
on 124 pages. Append ' | Keploy Docs' so the title differs from the h1.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): add alt text to images missing it (A8)

13 images across 8 doc pages (glossary GIFs + running-keploy screenshots)
had no alt attribute -> SEMrush 'Missing ALT' + weaker AI/accessibility
extraction. Add descriptive alt to each. utg-vscode-extension from the
audit list no longer exists on main, so it is not included.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): prettier formatting on api-testing-functions/variables

Pre-existing whitespace/list-indent/final-newline issues in these two
files (unrelated to the alt fix) would fail the prettier CI check once the
files are touched. Apply prettier so the PR is clean.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): prettier (2.8.8) on DocItem

Wrap the long articleImage fallback line and flatten the pre-existing
nested-ternary indentation so the swizzled DocItem is clean under the
CI-pinned prettier 2.8.8 (--check runs on changed files).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): format DocItem for CI prettier 3.9.6

The prettify_code workflow pins prettier_version 2.8.8, but creyD/
prettier_action's npx step actually installs and runs prettier 3.9.6.
3.x changed nested-ternary indentation, so the prior 2.8.8-flattened
schemaType ternary failed CI. Reformat to 3.9.6 (the version CI runs) so
--check passes; all other changed files are already 3.9.6-clean.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): specialize doc schema to TechArticle/APIReference (Doc2)

Every /docs page emitted a generic Article. Route API-reference pages
(api-testing-*, public-api, cli-commands, sdk-installation — by tag or
path) to APIReference, and default all other docs to TechArticle. Both are
Article subtypes accepting the same properties as articleSchema, so the
schema shape is unchanged — just a precise, AI/Google-friendly type.
Built HTML: 35 APIReference, 343 TechArticle, 0 generic Article from docs.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): unique meta descriptions for 6 pages (A7)

general-glossary + 3 glossary terms shared one boilerplate description;
share-tests copied the generate-api-tests description; k8s-proxy had a
wrong (NextJS/Drizzle) description. Give each a unique, accurate one to
clear SEMrush 'Duplicate meta descriptions'.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): prettier 3.9.6 on beta-testing.md

Pre-existing leading-space paragraphs (unrelated to the description
change) would fail the prettier CI check once the file is touched.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): add structured data to bespoke pages (Doc2)

/leadership, /security and /privacy-policy emitted zero page-level schema.
Add JSON-LD: leadership -> CollectionPage + ItemList of Person (name,
photo, Twitter, worksFor Keploy); security -> WebPage + security
ContactPoint (responsible-disclosure email); privacy-policy ->
DigitalDocument with dateModified. Verified in built HTML.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): emit FAQPage schema for FAQ docs (Doc2/AI4)

New dependency-free remark plugin extracts each ### question + following
answer prose from FAQ docs (keploy-explained/*-faq) and injects FAQPage
JSON-LD with Question/acceptedAnswer pairs, so they're eligible for FAQ
rich results and AI extraction. Built HTML: FAQPage with 14/11/12 Q&A on
the three FAQ pages.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): consolidate JSON-LD into one @id-linked entity graph (Doc2)

Every route emitted several disconnected Organization nodes: the site-wide
one plus copies inlined as SoftwareApplication.publisher, Article.author and
Article.publisher, which had drifted to two different logo URLs. Declare each
site-wide entity (Organization, WebSite, SoftwareApplication) once in
src/schema/siteEntities.js with a stable @id and reference it by @id
everywhere else, so consumers merge them into a single entity per page.

- docusaurus.config.js: three sibling ld+json blocks -> one @graph (siteGraph)
- DocItem: author/publisher/isPartOf reference the shared @ids; derive the
  Article @id from the trailing-slash canonical URL so it matches the URLs the
  glossary hub and bespoke pages emit; add a DefinedTerm on glossary term
  pages, keyed to the hub's DefinedTermSet @id (latest version only)
- bespoke pages: about -> AboutPage, index -> CollectionPage + ItemList
  (off GetStartedPaths' exported data), leadership -> CollectionPage of Person,
  privacy-policy -> DigitalDocument, security -> WebPage + Organization
  contactPoint on the shared Org @id
- glossary hub DefinedTerms gain the same @id as their term pages so the two
  listings resolve to one entity
- GlossaryCard uses Docusaurus Link (build-checked, trailingSlash-aware)
- scripts/verify-schema-graph.js: fail CI on invalid JSON-LD or dangling @id

Verified: clean build, verify-schema-graph reports 0 invalid / 0 dangling
across 479 pages / 972 blocks; no generic Article remains.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): resolve residual SEO audit items (A8, A9)

- samples-echo: two body sections used a single `#`, producing three h1s on
  the page (title + two). Demote both to `##` so the page has one h1 and a
  clean heading hierarchy (A8, multiple-h1).
- configuration-file: rewrite four in-body doc links from http:// to https://
  (the two remaining http:// occurrences are regex examples in a code block
  and are left as-is) (A9, HTTP-link-on-HTTPS-site).
- api-testing-auth-setup: the title "Auth Setup" was flagged as too short and
  its description was a near-duplicate stub; give it a descriptive title and a
  unique description (sidebar_label stays "Auth Setup") (A9, title-too-short).

Verified: clean build (onBrokenLinks: throw), verify-schema-graph 0 invalid /
0 dangling.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A5): expand thin SCM PR-agent page with capabilities + related links

integrations-pr-agent was ~120 words (flagged "low word count"). Add a
"What the PR Agent does" section and a Related block linking the AI test
generator, UTG PR agent, and CI/CD GitHub setup, so the page carries real
content and more than one internal link (A4).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A5): expand Windows/WSL install page with prerequisites + related links

windows-wsl was ~177 words. Add why-WSL context, a Prerequisites section,
clearer install steps, and a Related block (macOS/Linux install, CLI
reference, common errors) to fix the thin-content and single-internal-link
flags (A5/A4).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related Terms" cross-links to all 37 glossary pages

Glossary term pages each had only one internal link (SEMrush "pages with
only one internal link"). Add a "Related Terms" section to every term page
linking 3–4 conceptually related terms plus the glossary hub, so terms form
a connected cluster that both readers and AI engines can traverse.

Link targets use each term's real route slug (e.g. agile-testing.md serves
at /agile-unit-testing/, grey-box-testing.md at /gray-box-testing/), verified
by a clean build under onBrokenLinks: throw.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to running-keploy docs

Add a "Related" section (3–4 contextual internal links) to running-keploy feature docs that had 
only one internal link. Links use each page real route slug, verified by a clean build under 
onBrokenLinks: throw.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to quickstart sample apps

Add a "Related" section to quickstart sample-app guides, linking same-language / same-datastore 
samples and the language recommendation page. Routes verified against the build (slugs differ from 
filenames here).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to keploy-cloud docs

Add a "Related" section to keploy-cloud pages that had a single internal link, linking sibling 
cloud features. Build-verified routes.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to keploy-explained docs

Add a "Related" section to keploy-explained pages, linking sibling concept/FAQ/setup docs. 
Build-verified routes.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to ci-cd docs

Cross-link the GitHub, GitLab and Jenkins CI guides to each other and to the API-test CI/CD setup. 
Build-verified routes.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(A4): add "Related" cross-links to server install + SDK docs

Add a "Related" section to the install pages and language SDK pages, cross-linking the SDKs and 
install guides. Build-verified routes.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(AI4): add HowTo schema to CI/CD integration guides

Emit schema.org HowTo JSON-LD (visible={false}, no visual change) on the GitHub, GitLab and Jenkins 
guides so AI engines can extract the install-and-run steps.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(AI4): add HowTo schema to language SDK install guides

Emit HowTo JSON-LD (visible={false}) on the Go, Java, JavaScript and Python SDK pages covering 
agent setup and coverage merge steps.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs(AI4): add HowTo schema to Linux/Windows install guides

Emit HowTo JSON-LD (visible={false}) on the Linux, Windows and native Windows/WSL install pages.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* ci(vale): accept technical terms flagged on changed lines

Add gzip, Mux, Prisma, JWT, unbuggy, webhook, datastore, async, boolean,
quickstarts and _Transform to the Vale accept vocabulary. These appear in the
new "Related" link text and in prettier-normalized code lines, and are valid
terms the Vale spell-check does not recognise.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): add CollectionPage on hubs, LearningResource on quickstarts

A section index.md (e.g. /docs/concepts/) emitted a TechArticle despite being an index of child 
docs; it now emits a CollectionPage. Quickstart tutorial docs are additionally typed as 
LearningResource with learningResourceType so education-focused consumers and AI can classify them.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): add WebPage/BreadcrumbList/ItemList to application-development

The /docs/application-development/ page emitted zero structured data. Add a WebPage, a 
BreadcrumbList, and an ItemList of the supported operating systems, keyed to the shared site entity 
graph.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* feat(docs): emit community-channels ItemList on the home page

The community section rendered six official channels with no structured data. Emit an ItemList 
built from the same data the cards render, so search engines and AI can enumerate the channels.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* refactor(docs): centralize breadcrumb JSON-LD in a shared builder

The home, about, glossary and application-development pages each hand-built the same Home -> Docs 
breadcrumb prefix, repeating the site/docs URLs and the ListItem position numbering. Add 
breadcrumbList(trail) to siteEntities (the existing schema source of truth); callers pass only the 
crumbs beyond Docs. Removes the now-unused SITE/HOME_URL constants; built breadcrumbs are unchanged.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): add repo-hosted 1200x630 social card

The old fallback https://keploy.io/images/keploy-hero.png 404s (it hit the
landing app's 404 page), so every social preview and the Article-schema image
that reused it were broken. Commit a real 1200x630 card served from /docs/img/
so it can't break from a change in the landing repo.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): sync SEO title and social-image metadata in the doc theme

- Point og:image and the Article-schema image fallback at the repo-hosted
  /docs/img/keploy-docs-card.png instead of the 404ing landing-app URL.
- Drop the emoji title delimiter (put a rabbit in SERP titles) for a plain
  "|", and only append the "| Keploy Docs" suffix when the title stays within
  the 60-char SERP limit, so title hygiene doesn't create title-too-long.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* ci(docs): run the schema-graph verifier in the PR build check

The verifier existed but nothing ran it, so its 0-invalid/0-dangling figures
were a one-time local measurement. Add a verify-schema npm script and a build
step so JSON-LD regressions fail CI.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): harden the schema-graph guard

- Classify typed refs ({@type,@id}) as references, not definitions, so a typed
  pointer at an undefined @id is caught; seed each page's own og:url as defined
  so the self-referential mainEntityOfPage doesn't false-positive.
- Skip noindex pages via the built HTML instead of a hard-coded version list.
- Fail on any surviving generic Article node.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): correct leadership route in schema and permalink

The CollectionPage url and the Layout permalink used /Leadership (capital L),
but the route derived from leadership.js is /docs/leadership/ -- the capital
form 404s on case-sensitive hosting. Also rename the copy-pasted Security()
component to Leadership().

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): keep FAQ answers readable and drop the Related section

textOf concatenated list/table children with no separator, so list-based FAQ
answers fused into run-on text ("foobar") -- the exact thing AI extraction
reads. Separate list items as sentences, skip code/tables, and skip the
"## Related" section so its link list isn't captured as a bogus Q&A.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): add trailing slash to the SearchAction target

${DOCS_URL}search?q=... 302s to /docs/search/ and drops the query; use the
canonical trailing-slash form.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): add alt text to remaining v4 images

A8 sweep missed 19 alt-less images on the served version (VS Code extension
screenshots, hacktoberfest/server/gsoc icons, docker-tls). Add descriptive
alt, and give RowOfImages alt1/alt2 props so callers can supply it.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): unique meta descriptions for duplicate pages

go-mux-mysql and go-mux-sql shared an identical description, and agile-testing
carried the generic glossary boilerplate. These pages are live (renamed files
kept their old id: slug), so A7 was still open for them.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): schema-type accuracy in the doc theme

- Restrict APIReference to genuine API/CLI reference docs (public-api,
  cli-commands). The api-testing/api tags and the api-testing- path prefix name
  Keploy's product feature, not the page genre, and were typing ~28 task guides
  as APIReference -- often contradicting the HowTo block on the same page. Those
  now fall through to TechArticle.
- Give the Article node its own "@id" (pageUrl#article) so it is addressable in
  the @id graph, distinct from mainEntityOfPage's @id (the WebPage document).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): unique descriptions for the three Node.js sample apps

samples-node-mongo, node-express-mongoose and samples-express-mongoose all
shared one boilerplate description; give each a distinct in-band description.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): bring two edited descriptions within the 70-160 band

general-glossary (was 167) and api-testing-filter-suites (was 36) had their
descriptions changed by this PR but landed outside SEMrush's length band.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): format remarkFaqSchema per prettier 3.9.6

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): normalize en-dashes to em-dash or hyphen for Vale

Google.EnDash flags every en-dash (–). Convert spaced prose separators to an
em-dash (—, the repo's convention; Google.EmDash is disabled) and numeric
ranges / compounds (3–5, per–test-set) to a hyphen. Clears all EnDash errors
across v2/v3/v4 docs and two src components.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* docs: unlink NDJSON in the public API reference

Keep NDJSON as plain text instead of an external link.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* revert(docs): undo en-dash edits in non-Vale / pre-debt files

Vale only lints *.md, so the en-dashes in QuickStartTabs.js and WhatIsKeploy.js
were never flagged; and version-2.0.0/api-test-generator.md carries pre-existing
prettier debt in main. Editing these three only pulled that debt into the PR and
broke prettier — revert them to main. The .md en-dash fixes for served v3/v4
content stay.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* revert(docs): drop en-dash edits in noIndex v2/v3 pages

These 11 files were edited purely to swap en dashes for hyphens to
satisfy Vale's EnDash rule. The Vale linter is being removed (#896), so
the edits have no purpose, and versions 2.0.0/3.0.0 are noIndex:true so
they carry no SEO or AI-citation value. Reverting restores the PR to its
stated v4.0.0 scope and shrinks the diff.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): null-guard title before .length in DocItem

Line 193 read title.length and .test(title) unconditionally while every
schema block below gates on `pageUrl && title`, treating title as
possibly falsy. The original <title>{title}</title> was null-safe; add a
leading `title &&` so a doc without a resolved title falls through to the
bare title instead of throwing on .length.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* fix(docs): add prose lead-in so FAQ Q3 enters FAQPage schema

Q3's answer was a table only. remarkFaqSchema skips table nodes when
building answer text, so the answer came out empty and the question was
dropped from the emitted FAQPage JSON-LD. Add a one-sentence summary of
the protocol/format matrix above the table: the plugin now reads a
non-empty answer (Q3 is included with a citable answer), and human
readers get a quick summary before the table.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

* style(docs): prettier --write on the 8 files this PR touches

These 8 files carried pre-existing formatting drift on main; this PR
modifies them, pulling them into the changed-files prettier --check.
Formatting only (2.8.8, the CI pin) — no content changes.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

---------

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@nehagup

nehagup commented Sep 1, 2026

Copy link
Copy Markdown
Member

@dhananjay6561 please resolve conflicts.

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.

4 participants