diff --git a/.github/workflows/asset-budget.yml b/.github/workflows/asset-budget.yml
new file mode 100644
index 0000000000..ed87623059
--- /dev/null
+++ b/.github/workflows/asset-budget.yml
@@ -0,0 +1,78 @@
+name: Asset & Font Budget
+
+# Guardrails from the web-quality plan:
+# X1 - no committed image > 500 KB
+# X3 - no new render-blocking external font stylesheets (self-host instead)
+#
+# Both checks are scoped to the files CHANGED in the PR (diff against the base
+# branch), so pre-existing large assets (e.g. GIFs still awaiting MP4/WebM
+# conversion) never fail an unrelated PR — only newly added/modified files are
+# held to the budget.
+
+on:
+ pull_request:
+ branches: ["main"]
+
+jobs:
+ asset-budget:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: New/changed images must be under 500 KB (X1)
+ run: |
+ # checkout ran with fetch-depth: 0, so origin/ is already present
+ # with full ancestry for the three-dot merge-base below.
+ base="origin/${{ github.base_ref }}"
+ max=512000 # 500 KiB
+ fail=0
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue # skip deletions
+ size=$(wc -c < "$f")
+ if [ "$size" -gt "$max" ]; then
+ echo "::error file=$f::$f is $((size / 1024)) KB (> 500 KB). Optimize (WebP/AVIF) or convert to video before committing."
+ fail=1
+ fi
+ done < <(git diff --name-only --diff-filter=AMR "$base"...HEAD | grep -iE '\.(png|jpe?g|gif|webp|avif|svg)$' || true)
+ # AMR, not AM: a rename (R) that also re-encodes the file larger would
+ # otherwise slip the budget entirely, since git reports it as R.
+ if [ "$fail" -eq 0 ]; then echo "All new/changed images are within the 500 KB budget."; fi
+ exit "$fail"
+
+ - name: Block new render-blocking font stylesheets (X3)
+ run: |
+ base="origin/${{ github.base_ref }}"
+ # Match the actual render-blocking stylesheet URL (…googleapis.com/css…),
+ # not prose mentions of the domain, to avoid false positives on comments.
+ added=$(git diff --diff-filter=AMR "$base"...HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.json' '*.md' '*.mdx' '*.html' '*.css' \
+ | grep -E '^\+' | grep -E 'fonts\.googleapis\.com/css' || true)
+ if [ -n "$added" ]; then
+ echo "::error::New external Google Fonts reference detected. Self-host the font (src/fonts/*.woff2 + @font-face in src/css/custom.css) instead of a render-blocking :"
+ echo "$added"
+ exit 1
+ fi
+ echo "No new external font stylesheets."
+
+ - name: New local asset references in markdown must resolve (D2/D3)
+ run: |
+ base="origin/${{ github.base_ref }}"
+ # Docusaurus serves static/ at baseUrl /docs/, so an added
+ # src="/docs/
" must have a file at static/
. onBrokenLinks only
+ # validates links, not // src, so the "deletions
+ # break zero references" claim needs its own guard here.
+ fail=0
+ while IFS= read -r rel; do
+ [ -z "$rel" ] && continue
+ if [ ! -f "static/$rel" ]; then
+ echo "::error::Added asset reference /docs/$rel does not resolve to static/$rel"
+ fail=1
+ fi
+ done < <(git diff --diff-filter=AMR "$base"...HEAD -- '*.md' '*.mdx' '*.js' '*.jsx' '*.ts' '*.tsx' '*.html' \
+ | grep -E '^\+' | grep -oE 'src="/docs/[^"]+"' \
+ | sed -E 's#^src="/docs/##; s#"$##' | sort -u || true)
+ if [ "$fail" -eq 0 ]; then echo "All new /docs/ asset references resolve under static/."; fi
+ exit "$fail"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9939bec02a..5bf41db0df 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -51,7 +51,7 @@ jobs:
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
invalidation: /docs*
delete-removed: true
- cache: "public, max-age:86400"
+ cache: "public, max-age=86400"
private: true
- name: Submit docs URLs to IndexNow
diff --git a/docusaurus.config.js b/docusaurus.config.js
index ec3984ad19..38763a763d 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -3,6 +3,7 @@
import {themes as prismThemes} from "prism-react-renderer";
const path = require("path");
const fs = require("fs");
+const remarkImageSize = require("./src/remark/remarkImageSize");
import {visit} from "unist-util-visit";
const FontPreloadPlugin = require("webpack-font-preload-plugin");
@@ -29,28 +30,33 @@ const llmsFullRootContent = fs.readFileSync(
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
headTags: [
- // Google Fonts - DM Sans (loaded via headTags instead of CSS @import)
+ // DM Sans is now self-hosted (see src/css/custom.css @font-face). The
+ // render-blocking Google Fonts stylesheet + its two preconnects
+ // (fonts.googleapis.com / fonts.gstatic.com) were removed here to cut the
+ // font from the critical render path and drop us to <=4 preconnects.
+ // Preload the latin woff2 so it loads in parallel with the CSS instead of
+ // being discovered only after the stylesheet parses (html -> css -> font
+ // chain). crossorigin is required even same-origin for font preloads.
{
tagName: "link",
attributes: {
- rel: "preconnect",
- href: "https://fonts.googleapis.com",
- },
- },
- {
- tagName: "link",
- attributes: {
- rel: "preconnect",
- href: "https://fonts.gstatic.com",
+ rel: "preload",
+ href: "/docs/fonts/DMSans-latin.woff2",
+ as: "font",
+ type: "font/woff2",
crossorigin: "anonymous",
},
},
+ // DM Sans @font-face (self-hosted from static/fonts). Inlined here rather
+ // than in custom.css because webpack's css-loader can't resolve the stable
+ // /docs/fonts/ URL from within src/css. Variable font, weights 400-700,
+ // font-display:swap; latin + latin-ext subsets.
{
- tagName: "link",
- attributes: {
- rel: "stylesheet",
- href: "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&display=swap",
- },
+ tagName: "style",
+ attributes: {},
+ innerHTML: `
+@font-face{font-family:"DM Sans";src:url("/docs/fonts/DMSans-latin.woff2") format("woff2");font-weight:400 700;font-style:normal;font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
+@font-face{font-family:"DM Sans";src:url("/docs/fonts/DMSans-latin-ext.woff2") format("woff2");font-weight:400 700;font-style:normal;font-display:swap;unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}`,
},
// Preconnect tag
{
@@ -176,7 +182,9 @@ module.exports = {
},
}),
},
- // Meta Pixel Code
+ // Meta Pixel Code — fires eagerly (init + PageView) on load so conversion
+ // tracking is accurate from the first paint. Intentionally NOT deferred.
+ // SPA route changes re-fire PageView from src/metaPixelRouteTracker.js.
{
tagName: "script",
attributes: {},
@@ -219,6 +227,18 @@ fbq('track', 'PageView');`,
},
};
},
+ // D11: emit full source maps for the client bundle so first-party JS is
+ // debuggable in production (error tracking / DevTools). Server bundle keeps
+ // its default — no new dependency, no runtime impact, just extra .map files.
+ function clientSourceMapsPlugin() {
+ return {
+ name: "client-source-maps",
+ configureWebpack(_config, isServer) {
+ if (isServer) return {};
+ return {devtool: "source-map"};
+ },
+ };
+ },
"docusaurus-tailwindcss-loader",
[
"docusaurus-plugin-llms",
@@ -365,13 +385,13 @@ fbq('track', 'PageView');`,
copyright: `
@@ -484,6 +504,12 @@ fbq('track', 'PageView');`,
},
{},
],
+ // D5 (CLS): stamp intrinsic width/height onto raw tags so the
+ // browser reserves layout space. Docusaurus already sizes Markdown
+ // images natively, so this only touches hand-written .
+ // Dependency-free; skips remote/relative/webp/svg and never
+ // overwrites author dimensions.
+ remarkImageSize,
],
},
// Will be passed to @docusaurus/plugin-content-blog
@@ -504,6 +530,9 @@ fbq('track', 'PageView');`,
theme: {
customCss: require.resolve("./src/css/custom.css"),
},
+ // GA fires eagerly via the standard gtag preset (loads on page load +
+ // auto-tracks SPA route changes) so analytics are accurate from the
+ // first paint. Intentionally NOT idle-deferred.
gtag: {
trackingID: "G-LLS95VWZPC",
// Optional fields.
@@ -609,21 +638,13 @@ fbq('track', 'PageView');`,
clientModules: [require.resolve("./src/metaPixelRouteTracker.js")],
scripts: [
- {
- src: "/docs/scripts/feedback.js",
- async: true,
- defer: true,
- },
- {
- src: "/docs/scripts/clarity.js",
- async: true,
- defer: true,
- },
- {
- src: "/docs/js/apollo-init.js",
- async: true,
- defer: true,
- },
+ // Analytics loading (see src/metaPixelRouteTracker.js for the full strategy):
+ // - GA -> eager via the gtag preset (auto SPA tracking)
+ // - Meta Pixel -> eager via the inline snippet in headTags; SPA re-fire
+ // from the client module
+ // - Clarity + Apollo -> lazy, on first user interaction (client module)
+ // - Hotjar -> removed
+ // keploy's own first-party telemetry (~2 KiB) stays eager below.
{
src: "https://telemetry.keploy.io/sessions/sdk.js",
async: true,
@@ -631,16 +652,6 @@ fbq('track', 'PageView');`,
"data-endpoint": "https://telemetry.keploy.io/sessions/collect",
"data-source": "docs",
},
- /*{
- src: "/docs/scripts/chat.js",
- async: true,
- defer: true,
- },
- {
- src: "/scripts/fullstory.js",
- async: true,
- defer: true,
- },*/
],
};
diff --git a/src/components/Intro.js b/src/components/Intro.js
index d1f90ebc75..8306a24a31 100644
--- a/src/components/Intro.js
+++ b/src/components/Intro.js
@@ -7,6 +7,7 @@ function Languages() {
{
{link.type === "article" ? (
// Article Link Icon
{
) : (
// Other Link Icon (e.g., video, documentation)
+
+
{
{/* Narrative paragraph */}
- As developers, the hardest bugs were never the ones we could reproduce. They were the “works on my machine, breaks in prod” ones—because real user behavior and real dependencies don’t show up in unit tests.
+ As developers, the hardest bugs were never the ones we could
+ reproduce. They were the “works on my machine, breaks in prod”
+ ones—because{" "}
+
+ real user behavior and real dependencies don’t show up in unit
+ tests.
+
- We struggled to replicate real user behavior exactly like production on the leftmost part of the SDLC (local dev and CI/CD regression checks). That pain is what led to Keploy: capture real API behavior (and its dependencies) and make it replayable, so tests stay reliable across environments.
+ We struggled to replicate real user behavior exactly like
+ production on the leftmost part of the SDLC (local dev and
+ CI/CD regression checks). That pain is what led to Keploy: capture
+ real API behavior (and its dependencies) and make it replayable, so
+ tests stay reliable across environments.
@@ -92,8 +114,8 @@ export const WhatIsKeploy = () => {
id={card.id}
className={
card.tone === "primary"
- ? "group relative overflow-hidden rounded-2xl border border-orange-200 bg-gradient-to-br from-white to-orange-50/30 p-6 transition-all duration-300 hover:border-orange-400 hover:shadow-lg hover:shadow-orange-100/50 hover:no-underline dark:border-orange-900/50 dark:from-gray-900 dark:to-orange-950/10 dark:hover:border-orange-500 dark:hover:shadow-orange-900/20"
- : "group relative overflow-hidden rounded-2xl border border-purple-200 bg-gradient-to-br from-white to-purple-50/30 p-6 transition-all duration-300 hover:border-purple-400 hover:shadow-lg hover:shadow-purple-100/50 hover:no-underline dark:border-purple-900/50 dark:from-gray-900 dark:to-purple-950/10 dark:hover:border-purple-500 dark:hover:shadow-purple-900/20"
+ ? "group relative overflow-hidden rounded-2xl border border-orange-200 bg-gradient-to-br from-white to-orange-50/30 p-6 transition-all duration-300 hover:border-orange-400 hover:no-underline hover:shadow-lg hover:shadow-orange-100/50 dark:border-orange-900/50 dark:from-gray-900 dark:to-orange-950/10 dark:hover:border-orange-500 dark:hover:shadow-orange-900/20"
+ : "group relative overflow-hidden rounded-2xl border border-purple-200 bg-gradient-to-br from-white to-purple-50/30 p-6 transition-all duration-300 hover:border-purple-400 hover:no-underline hover:shadow-lg hover:shadow-purple-100/50 dark:border-purple-900/50 dark:from-gray-900 dark:to-purple-950/10 dark:hover:border-purple-500 dark:hover:shadow-purple-900/20"
}
>
@@ -133,7 +155,7 @@ export const WhatIsKeploy = () => {
-
+
@@ -145,7 +167,23 @@ export const WhatIsKeploy = () => {
Built for enterprise security & compliance
- Keploy is compliant with SOC 2 , ISO , GDPR , and HIPAA —with strong security controls designed for modern teams.
+ Keploy is compliant with{" "}
+
+ SOC 2
+
+ ,{" "}
+
+ ISO
+
+ ,{" "}
+
+ GDPR
+
+ , and{" "}
+
+ HIPAA
+
+ —with strong security controls designed for modern teams.
@@ -174,7 +212,7 @@ export const WhatIsKeploy = () => {
href="https://trust.keploy.io"
target="_blank"
rel="noreferrer"
- className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-all hover:from-indigo-700 hover:to-purple-700 hover:text-white hover:shadow-indigo-500/30 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:shadow-indigo-900/30 dark:focus-visible:ring-offset-gray-950"
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-all hover:from-indigo-700 hover:to-purple-700 hover:text-white hover:no-underline hover:shadow-indigo-500/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:shadow-indigo-900/30 dark:focus-visible:ring-offset-gray-950"
aria-label="Visit Keploy Trust Center"
>
Trust Center
@@ -193,4 +231,3 @@ export const WhatIsKeploy = () => {
};
export default WhatIsKeploy;
-
diff --git a/src/css/custom.css b/src/css/custom.css
index ae685e2506..62c965b8cc 100644
--- a/src/css/custom.css
+++ b/src/css/custom.css
@@ -1,4 +1,5 @@
-/* --- DM Sans loaded via headTags in docusaurus.config.js --- */
+/* --- DM Sans self-hosted below (see @font-face). Previously loaded via a
+ render-blocking Google Fonts stylesheet in docusaurus.config.js headTags. --- */
/* Also reveal copy button when the container or button has keyboard focus */
div[class*="codeBlockContainer"]:focus-within button[class*="copyButton"],
@@ -226,6 +227,12 @@ html[data-theme="light"] {
font-display: swap;
}
+/* DM Sans @font-face lives in docusaurus.config.js headTags as an inline
+